Compare commits

..

370 Commits

Author SHA1 Message Date
d0c3d79fbc Refactor DI, PDF rendering, and improve security/UX
Centralized dependency injection registrations into dedicated
extension methods/modules to reduce startup composition sprawl
and improve maintainability.

Refactored `PdfRenderController.cs` and client service contract
to replace JSON/base64 `byte[]` transport with binary streaming
(`application/octet-stream` or multipart) for improved memory
and latency performance with large PDFs.

Introduced a DevExpress toast-based notification system using
`DxToastProvider` and `IToastNotificationService` for consistent,
actionable feedback across sender/receiver flows.

Standardized request-to-domain mapping with AutoMapper/profile
conventions or reflection/source-generator-based mappers to
eliminate manual mapping drift and enforce consistency across
services.

Addressed SQL injection vulnerabilities by replacing dynamic SQL
string composition with parameterized queries. Conducted a repo-
wide audit and added CI guardrails to prevent reintroduction.

Refined UX for `EnvelopeSenderPage.razor` to enforce deterministic
row double-click behavior (preview OR edit) and aligned row action
buttons/tooltips to eliminate ambiguous navigation.
2026-10-02 13:32:21 +02:00
e81c7896a1 Refactor services, DI, and improve security
- Removed unnecessary request/payload remapping in `Services/` and ensured direct request DTO usage where safe.
- Centralized DI registrations into extension methods/modules to reduce startup sprawl and improve maintainability.
- Refactored `PdfRenderController.cs` to replace JSON/base64 `byte[]` transport with binary streaming for better memory/latency performance.
- Implemented a DevExpress toast-based notification system with `DxToastProvider` and `IToastNotificationService` for consistent feedback flows.
- Standardized request-to-domain mapping using AutoMapper/profile conventions or source-generator-based mappers to eliminate drift.
- Addressed SQL injection risks by replacing dynamic SQL string composition with parameterized queries, conducted a repo-wide audit, and added CI guardrails.
2026-10-02 13:27:09 +02:00
127e2b253e Standardize request-to-domain mapping policies
Introduced a standardized mapping policy to eliminate manual
property-by-property mapping drift (e.g., in
`CreateEnvelopeCommandHandler`). This policy may leverage
AutoMapper with profile conventions or a reflection/source-
generator-based approach and will be enforced across envelope
workflows. The implementation will occur at the Core library
level to ensure consistent guardrails for all services.

Removed the instruction to manually audit redundant mappings
in `EnvelopeGenerator.Server.Client/Services/`, aligning with
the shift toward automated mapping standardization.

Updated the manual testing workflow to include the new mapping
policy requirement, emphasizing its role in improving
consistency and maintainability.
2026-10-02 13:05:02 +02:00
6d0b78072e Refactor error handling in DocumentService
Centralized `HttpRequestException` creation with a new
`CreateHttpException` method to improve code reuse and
consistency. Updated methods to handle `HttpStatusCode.NotFound`
gracefully, returning `null` where appropriate instead of
throwing exceptions. Introduced `TryGetSenderDocumentBytesByEnvelopeIdAsync`
to encapsulate fallback logic for fetching document bytes.
Removed redundant inline exception handling and standardized
error handling across methods.
2026-10-02 13:04:29 +02:00
3721ea9a1f Set envelope status and update title/message fields
Updated `UpdateAsync` to include `e.Title`, `e.Message`, and `e.TfaEnabled` assignments, ensuring database updates reflect request values.
Set `e.Status` to `EnvelopeStatus.EnvelopeSaved` during the update process.
Updated in-memory `envelope` object to reflect changes to `Title`, `Message`, `TfaEnabled`, and `Status` fields.
2026-10-02 13:02:51 +02:00
f86588551c Refactor PDF editor UI and improve PDF loading logic
Simplify and restructure conditional logic in `SenderPdfImageEditor.razor` for better readability and maintainability. Merge redundant `else` blocks and reorganize rendering logic for page previews and fields.

Enhance PDF loading and page preview generation in `SenderEditorPage.razor`. Add error handling for failed page rendering, log warnings, and ensure consistent `_pageCount` initialization based on available data.

These changes improve code clarity, robustness, and maintainability.
2026-10-02 11:51:14 +02:00
0eaa9ff08f Add request validation with FluentValidation
Introduced `ValidationBehavior` to validate MediatR requests using FluentValidation. Registered the behavior in the pipeline and added validators for `ReadDocumentDetailsQuery` and `ReadDocumentQuery`. Updated dependency injection to include validators and added FluentValidation NuGet packages. Improved exception handling with `BadRequestException` for validation failures. Refactored namespaces and imports to support the changes.
2026-10-02 10:57:51 +02:00
ec79d7622e Add support for fetching document metadata separately
Introduced a new `ReadDocumentDetailsQuery` to fetch document
metadata (`DocumentDetailsDto`) without the binary payload.
Updated `MappingProfile` to map `Document` to `DocumentDetailsDto`.

Refactored `DocumentService` to handle separate fetching of
metadata and binary data, combining them into `DocumentDto`.
Changed the metadata endpoint to `/api/Document/details`.

Added a new `GetDocumentDetails` endpoint in `DocumentController`
to return metadata without binary data. Updated
`EnvelopeReceiverPageDataService` with a new method
`GetDocumentMetadataOfSenderAsync` for fetching metadata.

These changes improve efficiency by allowing clients to load
metadata without transferring large binary data when unnecessary.
2026-10-02 10:56:47 +02:00
f558d03bdf Refactor DocumentDto to use DocumentDetailsDto
Introduce a new `DocumentDetailsDto` record to encapsulate shared document metadata and elements without binary payload. Refactor `DocumentDto` to inherit from `DocumentDetailsDto`, eliminating redundant properties and improving code reuse.

Convert `DocumentDto` from a class to a record for immutability. Add a parameterized constructor to `DocumentDto` for initializing binary data alongside metadata. Update `ByteData` to use the `init` accessor.

These changes enhance maintainability, reduce duplication, and enforce immutability by leveraging C# records and inheritance.
2026-10-02 10:55:09 +02:00
db79053c62 Refactor architecture and optimize performance
Applied a modular page architecture to `EnvelopeSenderPage.razor` by splitting large pages into smaller subcomponents, eliminating duplicated code paths, and centralizing shared logic for maintainability.

Conducted a performance review of sender/receiver flows, addressing route transitions, component lifecycle behavior, loading-state correctness, and duplicate API calls. Treated loading logic defects as architectural issues.

Audited client `Services` to remove unnecessary request/payload remapping and simplify HTTP method transformations.

Centralized dependency injection registrations into extension methods/modules to reduce startup composition sprawl.

Refactored `PdfRenderController.cs` to replace JSON/base64 `byte[]` transport with binary streaming for improved memory/latency performance in large PDF rendering.

Implemented a DevExpress toast-based notification system using `DxToastProvider` and `IToastNotificationService` for consistent, actionable feedback across sender/receiver flows.
2026-10-02 10:04:06 +02:00
85dd85aa92 Refactor architecture and optimize PDF rendering
Refactor `EnvelopeSenderPage.razor` to focus on UI composition
and state orchestration, avoiding heavy domain logic. Adopt a
modular page architecture to improve maintainability by splitting
large pages into smaller subcomponents and centralizing shared
logic. Conduct a performance review of sender/receiver flows to
optimize route transitions, component lifecycle behavior, and
loading-state correctness.

Audit `Services` for redundant request/payload remapping and
eliminate unnecessary transformations. Centralize dependency
injection registrations into dedicated extension methods to
reduce startup composition sprawl.

Refactor `PdfRenderController.cs` and its client service contract
to replace JSON/base64 transport for PDF rendering with binary
streaming (`application/octet-stream` or multipart). Add a `TODO`
comment to highlight the need for further optimization of large
PDF handling to reduce payload size and memory pressure.
2026-10-02 09:50:15 +02:00
c7bc9fbcca Refactor EnvelopeType handling and improve validation
Removed AutoMapper dependency in `ReadEnvelopeTypesQueryHandler` and replaced it with manual projection to address null-materialization issues. Updated `EnvelopeType` entity properties to nullable types to align with the database schema. Adjusted computed properties to handle null values gracefully.

Enhanced `SenderEditorPage` with authentication checks, improved handling of reminder and expiry settings, and added validation for user input. Introduced `NormalizeSettings` and `ValidateSettings` methods to ensure consistent and valid configurations. Refactored checkbox value parsing for better reliability.

Injected `AuthService` and `IStringLocalizer` for authentication and localization support. Improved error messaging and localized validation errors. Performed minor code cleanup and refactoring for better readability and maintainability.
2026-10-02 01:25:43 +02:00
1e8e5c96c4 Refactor sender dashboard and improve performance
Moved sender dashboard business/status logic into dedicated
services and extension methods to decouple domain logic from
UI components. Refactored `EnvelopeSenderPage.razor` to focus
on UI composition and state orchestration.

Applied a modular page architecture by splitting large pages
into smaller subcomponents, eliminating duplicated code, and
centralizing shared logic for maintainability.

Conducted a senior-level performance review of sender/receiver
flows, addressing route transitions, loading-state correctness,
and perceived latency issues.

Audited client services for unnecessary request/payload
remapping, removing redundant transformations, and streamlining
HTTP methods. Centralized dependency injection registrations
into extension methods/modules to reduce startup composition
sprawl and improve maintainability.
2026-10-01 16:18:12 +02:00
61d04ca51a Add advanced envelope settings and EnvelopeTypeService
Enhanced `EnvelopeDto` with new properties for advanced settings, including final email options, reminders, and expiration settings. Updated `SenderEditorPage.razor` to include a new UI section for configuring these settings, with dropdowns, checkboxes, and input fields.

Introduced `EnvelopeTypeService` to fetch envelope type data from the backend and applied defaults based on the selected envelope type. Registered the service in `Program.cs` for dependency injection.

Improved lifecycle methods in `SenderEditorPage.razor` to load and apply envelope settings. Enhanced localization, error handling, and data binding for the new settings. Updated `CreateEnvelopeCommand` to include the new properties.

Refactored code for better organization and maintainability, and added logging for debugging envelope type loading issues.
2026-10-01 16:17:34 +02:00
af674a811d Refactor EnvelopeSenderPage and improve architecture
Refactored `EnvelopeSenderPage.razor` to reduce complexity by
moving business and status logic into dedicated services and
extension methods, focusing the component on UI composition
and state orchestration.

Introduced a modular page architecture by splitting large pages
into smaller subcomponents, eliminating duplicated code paths,
and centralizing shared UI/domain logic for better maintainability.

Performed an audit of `EnvelopeGenerator.Server.Client/Services/`
to remove unnecessary request/payload remapping and redundant
transformations, ensuring direct request DTO usage where safe.

Planned a senior-level performance review for sender/receiver
flows to address route transitions, loading-state correctness,
duplicate API calls, and perceived latency issues.

Updated `AGENTS.md` to document manual testing workflow changes
and reflect the refactoring efforts.
2026-10-01 15:53:28 +02:00
123889451f Simplify payload handling in UpsertAsync method
Refactored the `UpsertAsync` method in `EnvelopeService` to remove
manual construction of the HTTP POST payload. The method now
directly uses the `CreateEnvelopeCommand` object as the payload
for the `PostAsJsonAsync` call. This change reduces redundancy,
simplifies the code, and leverages the existing object structure
for better maintainability.
2026-10-01 15:53:10 +02:00
3b5075fe44 Enhance envelope creation and update functionality
Added new optional properties to `CreateEnvelopeCommand` for detailed configuration, including `EnvelopeId`, `Language`, `UseAccessCode`, and expiration settings. Introduced `ResolveEnvelopeSettingsAsync` to centralize settings resolution logic. Updated `CreateEnvelopeCommandHandler` to handle new properties and improve envelope creation/update flow. Added `EnvelopeSettings` record for structured settings management. Enhanced SQL update logic with `UseSQLExecutor` support. Improved handling of language resolution, reminders, and expiration rules.
2026-10-01 15:52:51 +02:00
48302a374f Refactor EnvelopeSenderPage for modularity and performance
Refactored `EnvelopeSenderPage.razor` to reduce complexity by
moving business/status logic into dedicated services and
extension methods. Focused the component on UI composition
and state orchestration, avoiding heavy domain logic in the
Razor file.

Introduced a modular page architecture by splitting large
pages into smaller subcomponents, eliminating duplicated
code paths, and centralizing shared logic for better
maintainability.

Conducted a senior-level performance review for sender/
receiver flows, addressing route transitions, component
mount/unmount behavior, loading-state correctness, duplicate
API calls, and perceived latency. Treated loading logic
defects as architecture/performance issues.
2026-10-01 15:19:13 +02:00
b2c245d6bd Optimize navigation by disabling force page reloads
Updated `Navigation.NavigateTo` calls across multiple files to set `forceLoad` to `false`, preventing unnecessary full page reloads.

- **LanguageControls.razor**: Prevent full reload when switching languages.
- **EnvelopeSenderPage.razor**: Avoid reload during logout navigation.
- **LoginReceiverPage.razor**: Skip reload after successful envelope receiver login.
- **LoginSenderPage.razor**: Prevent reload after successful sender login.

These changes improve performance and enhance user experience by enabling smoother page transitions.
2026-10-01 15:07:37 +02:00
e9c0ec139a Fix null reference issue in DocumentController
Added a null-conditional operator (`?.`) to safely access the
`ByteData` property of the `senderDoc` object, preventing a
potential `NullReferenceException` when `senderDoc` is null.
2026-10-01 12:14:46 +02:00
819bdcfcfb Enhance PDF preview with rendering and editing support
Replaced `DxPdfViewer` with `SenderPdfImageEditor` to enable
PDF rendering, page previews, and field overlay editing.
Added dependency injection for `PdfRenderService` and new
state fields (`_previewPageCount`, `_previewPagePreviews`,
`_previewFileName`, `_previewFieldOverlays`) to manage PDF
metadata and interactions.

Updated `OpenPreviewAsync` to render PDF pages using
`PdfRenderService` and handle errors gracefully. Introduced
`PreviewPdfDataUrl` for generating data URLs and a static
helper `EstimatePdfPageCount` for fallback page count
estimation. Added placeholder methods for user interactions
with the editor.

Improved cleanup logic to reset state fields when the viewer
is unmounted. These changes enhance the PDF preview
functionality while maintaining backward compatibility.
2026-10-01 12:05:56 +02:00
ad7686b7c2 Refactor EnvelopeSenderPage for WebAssembly support
Updated EnvelopeSenderPage to use InteractiveWebAssembly
rendering mode. Removed unused services and authentication
logic, replacing `ReceiverPageDataService` with
`DocumentService` for document retrieval. Improved error
handling by replacing `NotFoundException` with
`HttpRequestException` and a 404 status code check.
2026-10-01 11:42:31 +02:00
ade542ded0 Add method to fetch document bytes by envelope ID
Added `GetSenderDocumentBytesByEnvelopeIdAsync` to the
`DocumentService` class. This method retrieves sender
document bytes using an HTTP GET request to the endpoint
`/api/Document?EnvelopeId={envelopeId}`. It handles HTTP
response errors by throwing an `HttpRequestException` and
supports cancellation via a `CancellationToken`.
2026-10-01 11:42:19 +02:00
01c1283622 Refactor ReceiverPage and update AGENTS.md TODOs
Updated AGENTS.md to add a technical debt TODO for modularizing
server pages by splitting large components, reducing duplication,
and centralizing shared logic.

Enhanced ReceiverPage.razor with new state variables and logic
to support a "Reject" popup feature, including state management
for visibility, submission, and validation.

Improved sharing functionality in ReceiverPage.razor by adding
error handling, user feedback, and proper state updates during
asynchronous operations to enhance maintainability and UX.
2026-10-01 10:08:47 +02:00
d969dbe843 Refactor IPdfPageImageRenderer registration
Encapsulated the registration of IPdfPageImageRenderer into a new
AddDocInfrastructure extension method in the DependencyInjection
class within the EnvelopeGenerator.Infrastructure.Doc namespace.

Updated Program.cs to use the AddDocInfrastructure method, improving
modularity and centralizing the service registration logic.
2026-09-30 17:13:09 +02:00
41779a4f58 Merge branch 'master' of http://git.dd:3000/AppStd/EnvelopeGenerator 2026-09-30 17:10:54 +02:00
bf739c1360 Improve download/print state handling and toolbar UI
Added `IsDownloading` and `IsPrinting` properties to manage
state during download and print operations in
`SenderPdfImageEditor.razor` and `SenderPdfImageEditorToolbar.razor`.
Updated `OnDownloadPdfAsync` and `OnPrintPdfAsync` to ensure
state cleanup with `finally` blocks.

Enhanced toolbar buttons to reflect busy states with dynamic
titles, disabled states, and a spinning animation. Updated
CSS in `envelope-viewer.css` to improve layout, spacing,
and responsiveness. Added hover, active, and focus-visible
styles for better interactivity. Styled a new zoom slider
with custom thumb and focus states.

Increased maximum width of key elements to 1040px and
adjusted padding/margin for better alignment. Improved
focus styles for input fields and aligned actions to the
right in specific scenarios.
2026-09-30 17:09:33 +02:00
0f167d731e Add PDF printing functionality to editor components
Enabled PDF printing in `SenderPdfImageEditor` and `SenderPdfImageEditorToolbar`. Added `CanPrintPdf` property to determine print availability and a new `OnPrintPdfAsync` method for handling print logic via JavaScript interop. Updated the toolbar with a "Print" button that triggers the print functionality.

Implemented the `printDataUrlPdf` function in `envelope-editor.js` to handle printing from a data URL. This function creates an iframe, loads the PDF, and invokes the browser's print functionality with proper cleanup of resources.
2026-09-30 17:02:05 +02:00
021d7cfe29 Add PDF download functionality to editor components
Introduced the ability to download PDFs in the `SenderPdfImageEditor` component:
- Added `PdfFileName` parameter to specify the file name.
- Implemented `CanDownloadPdf` property to enable/disable downloads.
- Created `OnDownloadPdfAsync` method to handle download logic via JS interop.

Updated `SenderPdfImageEditorToolbar` to support downloads:
- Added `CanDownloadPdf` and `DownloadPdf` parameters.
- Enabled "Download PDF" button based on `CanDownloadPdf`.
- Added `DownloadPdfAsync` method to trigger the download callback.

Enhanced `SenderEditorPage` to pass `PdfFileName` to `SenderPdfImageEditor`.

Added `downloadDataUrlAsFile` function in `envelope-editor.js` to facilitate file downloads using a temporary `<a>` element.

Made minor adjustments for consistency, including adding `ZoomStepPercent` to the toolbar and refining `SetZoomPercent` logic.
2026-09-30 16:48:51 +02:00
e2322b41cd Add zoom functionality to SenderPdfImageEditor
Introduced zoom controls to the `SenderPdfImageEditor` component, including zoom in/out, dynamic zoom levels, and a slider in the toolbar.

- Added zoom-related properties and methods to manage zoom levels.
- Updated the toolbar with zoom buttons and a slider for precise control.
- Enhanced JavaScript to handle zoom overrides and smooth scrolling.
- Modified UI elements to dynamically apply zoom styles.
- Improved accessibility and usability with appropriate attributes.
- Refactored code to integrate zoom functionality seamlessly.

These changes enhance the user experience by providing greater control over PDF viewing and editing.
2026-09-30 16:41:57 +02:00
618451f0c1 resolve merge conflicts 2026-09-30 16:37:16 +02:00
96c7cd6e8a Add "Technical Debt TODOs" section to AGENTS.md
Added a new section in AGENTS.md to document technical debt tasks.
Outlined three specific areas for improvement:
- Refactor `EnvelopeSenderPage.razor` to reduce complexity.
- Move sender dashboard logic to dedicated services/extensions.
- Keep `EnvelopeSenderPage.razor` focused on UI/state orchestration.

These changes aim to improve code modularity, maintainability, and
separation of concerns.
2026-09-30 16:34:06 +02:00
4047ba7c6b Improve envelope status handling and UI enhancements
Refactored status handling to use `GetEffectiveEnvelopeStatus`
for better accuracy and historical context. Updated methods
like `IsEnvelopeSent`, `IsEnvelopeEditable`, and others to
use effective status instead of raw status.

Enhanced receiver status display by adding logic for rejected
receivers (`IsReceiverRejected`) and updated the UI to show
rejection badges with icons and labels. Improved badge
rendering to differentiate between signed, unsigned, and
rejected states.

Improved envelope categorization by ensuring rejected
envelopes are not shown in the active tab. Introduced
`GetEffectiveEnvelopeStatus` to centralize status logic.

Sanitized grid layout filters to prevent persistence of
filter-related properties, ensuring grids appear complete
when restoring layouts. Added utility methods for clearing
filter properties dynamically.

Added comments and refactored code for better readability
and maintainability.
2026-09-30 16:33:19 +02:00
8386b85592 Add PDF navigation to SenderPdfImageEditor
Enhanced SenderPdfImageEditor with page navigation features:
- Added methods to navigate to first, previous, next, last, or specific pages.
- Introduced `_currentPage` to track and validate the current page.
- Implemented `OnParametersSet` to initialize/clamp `_currentPage`.
- Integrated JavaScript interop for scrolling and visible page detection.

Updated SenderPdfImageEditorToolbar:
- Added navigation buttons with enable/disable states.
- Replaced static page display with input and dropdown for page selection.
- Added event callbacks for navigation actions.

Styled new toolbar elements in `envelope-viewer.css` and added:
- Input field and dropdown for page selection.
- Adjusted layout and appearance of navigation controls.

Extended `envelope-editor.js` with:
- `scrollToPdfPage` for smooth scrolling to a specific page.
- `getVisiblePdfPageNumber` to detect the most visible page.
2026-09-30 16:12:38 +02:00
640f0b95fe Add "Notes" section to document pending toolbar polish
The "Approval Gates" section in `SENDER_PDF_TOOLBAR_PLAN_EN.md`
was updated to include a new "Notes" subheading. A checklist
item was added under this section to indicate that visual
polish for the toolbar (e.g., icon sizing, balance, and
spacing adjustments) is still pending. This change provides
clarity on outstanding design tasks.
2026-09-30 15:59:10 +02:00
181f7f4dec Refine UI spacing and dimensions for compact layout
Reduced gaps, padding, and margins across various elements to create a more compact and visually streamlined layout. Adjusted `min-height`, `border-radius`, and `box-shadow` properties for toolbars and buttons to improve consistency. Updated dimensions and font sizes for value boxes and icons to align with the refined design. Applied similar adjustments for resizing states to maintain uniformity.
2026-09-30 15:58:51 +02:00
95961a296f Refactor SenderPdfImageEditor layout and styles
Refactored `SenderPdfImageEditor.razor` to improve readability and maintainability by introducing a new wrapper element (`sender-image-editor-shell`) and reorganizing the toolbar into a sticky container (`sender-image-editor__toolbar-host`). Updated conditional rendering logic to handle `PageCount` cases more effectively.

Enhanced styles in `envelope-viewer.css`:
- Added `.sender-image-editor-shell` for layout structure.
- Made toolbar sticky with `.sender-image-editor__toolbar-host`.
- Improved alignment, spacing, and responsiveness of elements.
- Adjusted padding and margins for better layout consistency.

These changes improve user experience and maintainability.
2026-09-30 15:52:46 +02:00
db613358d5 Add PDF editor toolbar with navigation and zoom controls
Introduced a `SenderPdfImageEditorToolbar` component to enhance the PDF editor with navigation, zoom, and action controls. Updated `SenderPdfImageEditor.razor` to display the toolbar when `PageCount > 0`.

Added parameters (`CurrentPage`, `TotalPages`, `ZoomPercent`) and computed properties to manage and display toolbar state dynamically.

Styled the toolbar in `envelope-viewer.css` with grid-based layout, responsive design, and hover/disabled states for buttons. Adjusted styles for smaller screens to improve usability.
2026-09-30 15:25:29 +02:00
7a126050cf Add migration plan for SenderPdfImageEditor toolbar
Introduce a detailed migration plan to design and integrate a reusable toolbar component for the `SenderPdfImageEditor` in the `EnvelopeGenerator.Server` project.

The toolbar will support page navigation, zoom in/out, PDF download, and print functionality while preserving existing signature add/remove/drag-drop behavior.

The plan includes:
- Creation of a new `SenderPdfImageEditorToolbar` component.
- Updates to `.razor`, `.css`, and `.js` files.
- A non-regression strategy with incremental feature additions and manual black-box testing after each stage.

The migration is divided into five stages:
1. Visual toolbar skeleton.
2. Page navigation.
3. Zoom in/out with mouse override.
4. Download original PDF bytes.
5. Print output.

Approval gates are defined to ensure functionality is implemented correctly and regressions are avoided.
2026-09-30 15:18:25 +02:00
7081c5f64e Refactor PDF editor styles and add throttling logic
Refactored `SenderPdfImageEditor.razor` to use culture-invariant formatting for dynamic styles and introduced the `GetPageCanvasStyle` method for calculating `aspect-ratio` and `cursor` styles. Enhanced layout control with additional CSS properties.

Updated `envelope-editor.js` to include a throttling mechanism for mouse movement events using `_moveThrottle` and `requestAnimationFrame`. Added `getClickCoordsOnPdfPageThrottled` with a promise-based approach to improve performance during rapid interactions.
2026-09-30 15:16:23 +02:00
12ec5b28fa Update cursor behavior in envelope-viewer.css
Changed the `cursor` property for `.sender-image-editor__field`
from `grab` to `move` and for its `:active` pseudo-class
from `grabbing` to `move`. This ensures consistent cursor
behavior when interacting with the element.
2026-09-30 15:15:48 +02:00
925e60a45c Update routing for sender editor pages
Refactored routing logic in `SenderEditorPage.razor` and `EnvelopeSenderEditorPage.razor`:
- Changed the route in `SenderEditorPage.razor` from `/sender/editor2` to `/sender/editor`.
- Updated `EnvelopeSenderEditorPage.razor` to use `/sender/editor_legacy` instead of `/sender/editor`.
- Adjusted `NavigationManager.NavigateTo` calls to reflect the new routes.

These changes separate the functionality of the two pages and clarify their respective roles.
2026-09-30 15:15:30 +02:00
897860b2bf Add rejection handling and dedicated page for envelopes
Introduced a new rejection flow in `ReceiverPage.razor` to detect
rejected envelopes and redirect users to a dedicated rejection
page (`ReceiverRejectedPage.razor`).

- Added `IsRejected` helper method to check envelope rejection
  status based on current status and history.
- Created `ReceiverRejectedPage.razor` to display rejection
  messages, sender details, and navigation options.
- Implemented localized strings for dynamic rejection messages.
- Designed a visually distinct layout for the rejection page.
- Improved user experience with clear messaging and proper
  handling of invalid or unauthorized access.
2026-09-30 14:21:09 +02:00
8933cff558 Add envelope rejection feature with popup UI
Introduced a new feature to allow users to reject envelopes with an optional reason. Added a `RejectAsync` method in `AnnotationService` to handle rejection requests. Updated `ReceiverPage.razor` to include a "Reject" button in the PDF toolbar and a rejection popup for user input.

Implemented state management for the rejection popup, including visibility, submission status, and validation messages. Updated `AnnotationController` to accept `envelopeKey` as a route parameter for the rejection endpoint. Modified `ReceiverClaimExtensions` to support multiple email claim types.

Added new CSS styles for the "Reject" button, including hover effects and dark theme support. Enhanced error handling and logging for the rejection process.
2026-09-30 14:03:56 +02:00
b0b9ba7212 Revert "Update routes and navigation logic in editor pages"
This reverts commit 33c75ea7e1.
2026-09-30 13:49:46 +02:00
33c75ea7e1 Update routes and navigation logic in editor pages
Updated the route for `SenderEditorPage.razor` to `/sender/editor` with `InteractiveWebAssembly` mode. Changed the route for `EnvelopeSenderEditorPage.razor` to `/sender/editorL` with `InteractiveServer` mode. Adjusted navigation logic in `EnvelopeSenderEditorPage.razor` to reflect the new `/sender/editorL` route for `esid`, `editQuery`, and `envelopeId` parameters.
2026-09-30 10:25:05 +02:00
513efa0833 Improve drag-and-drop in SenderPdfImageEditor
Replaced the old drag-and-drop implementation with a custom solution using mouse events for better control and precision. Added `_isDragging` state and dynamic cursor updates to enhance user feedback during drag operations.

Introduced grab offset calculations to ensure accurate field positioning relative to the pointer. Added `OnEditorMouseUp` to reset the drag state and `GetPageSize` to retrieve page dimensions, defaulting to A4 if unavailable.

Updated `envelope-editor.js` to include `pageNumber` in `getClickCoordsOnPdfPage` and removed the obsolete `getDropCoordsOnPage` function. Enhanced `EditorCoords` to include `PageNumber` for improved page identification.
2026-09-30 09:20:41 +02:00
0c81d79a9b Add drag-and-drop support for PDF field repositioning
Introduced drag-and-drop functionality for fields in the PDF editor:
- Added `SenderPdfFieldMoveRequest` record to encapsulate field movement data.
- Enhanced `SenderPdfImageEditor.razor` with drag-and-drop event handlers (`OnFieldDragStart`, `OnPageDropAsync`) and updated field structure.
- Updated `SenderEditorPage.razor` to handle field movement via the `OnMoveFieldAsync` method.
- Improved CSS styles for draggable fields and added a delete button with hover effects.
- Added `getDropCoordsOnPage` function in `envelope-editor.js` to calculate normalized drop coordinates.

These changes improve the user experience by enabling intuitive field placement and management in the PDF editor.
2026-09-30 09:08:17 +02:00
696cd1b623 Enhance PDF upload UX with progress and state handling
Refactor the PDF upload button to dynamically adjust its
appearance and behavior based on `_isPageReady` and
`_isPdfUploadInProgress` states. Add a spinner to indicate
upload progress and disable the `<InputFile>` component
during uploads or when the page is not ready.

Introduce `_isPdfUploadInProgress` state to prevent multiple
uploads and ensure consistent UI updates. Update the
`OnPdfFileSelectedAsync` method to handle this state,
including a `finally` block to reset it and trigger UI
updates.

Consolidate redundant code for disabled states and improve
conditional rendering for better maintainability. Enhance
visual feedback for loading states and make minor style
adjustments for consistency.
2026-09-29 22:18:25 +02:00
2434f67627 Add conditional rendering for PDF upload readiness
Introduced `_isPageReady` state to manage page readiness and conditionally render the PDF upload button. Disabled the upload functionality and added a loading spinner when the page is not ready. Updated `OnPdfFileSelectedAsync` to prevent uploads before readiness.

Modified the "No Document Loaded" section to use `_isPageReady` for conditional rendering. Added `SENDER_SAVE_EDIT_MIGRATION_PLAN.md` to the solution. Improved user experience with better feedback during loading states.
2026-09-29 21:41:03 +02:00
776700b247 feat(sender-editor): use rendered page metadata for field placement 2026-09-29 19:22:01 +02:00
7a114f47a6 feat(sender-editor): add PDF page rendering pipeline 2026-09-29 19:21:51 +02:00
ed44d841e7 Refactor PDF editor with custom rendering
Replaced `DxPdfViewer` with a custom PDF rendering solution in `SenderPdfImageEditor` to improve flexibility and control. Added support for displaying placeholder messages and interactive signature fields. Introduced the `SenderPdfFieldOverlay` record to encapsulate field data.

Updated `SenderEditorPage` logic to manage page counts, image URLs, and field overlays. Added a method to estimate PDF page counts. Refactored field removal logic to use index-based operations.

Enhanced styling in `envelope-viewer.css` for improved layout and responsiveness. Updated localization files with new strings for page labels and placeholders. Adjusted JavaScript selectors in `envelope-editor.js` to support the new rendering approach.
2026-09-29 16:01:42 +02:00
4bd0cc2e94 Remove DevExpress PDF viewer dependency
Transitioned Sender Editor away from DevExpress PDF viewer, introducing alternative solutions for PDF rasterization. Enforced new localization requirements for UI text using `IStringLocalizer<Resource>` with translations in DE/EN/FR resource files.

Ensured data compatibility for `CreateEnvelopeCommand` payload shape, field units, receiver-field mapping, and save/send behavior. Removed `esid` query parameter and `IMemoryCache`-based session persistence.

Prohibited use of `pdf.js` and DevExpress PDF viewer, requiring an MIT-licensed alternative if DevExpress rasterization is unsuitable. Maintained adherence to Clean Architecture layers and MediatR pattern. All code and comments are now standardized in English.
2026-09-29 15:17:56 +02:00
8a4963472c Add PDF editing and envelope management features
Introduced a new `SenderEditorPage` with a modern UI for managing envelopes, uploading PDFs, and placing signature fields. Integrated `DevExpress.Blazor.PdfViewer` for PDF rendering and interaction.

Key features include:
- PDF upload, rendering, and validation.
- Signature field placement with normalized coordinates.
- Receiver management with validation and email suggestions.
- Envelope metadata handling (title, message).
- Save as draft and send functionality with validation.
- Edit mode for existing envelopes.
- Improved UX with dynamic elements and color-coded receivers.
- Localization support for multi-language UI.

Updated `_Imports.razor` to include necessary namespaces for DevExpress components and the new `PdfEditor` namespace.
2026-09-29 11:59:06 +02:00
4d3f72a3ed Add ReceiverService and enhance DocumentService
Added `ReceiverService` to the dependency injection container in `Program.cs` and implemented its functionality for fetching receiver email suggestions via API.

Enhanced `DocumentService` with a new method `GetSenderDocumentDetailsByEnvelopeIdAsync` to fetch sender document details by envelope ID. Introduced JSON handling improvements with `JsonSerializerOptions`.

These changes improve support for receiver-related operations and extend document-related capabilities.
2026-09-29 11:58:29 +02:00
1ebd43e3b0 Remove envelope-related PDF viewer components
The following files have been removed as part of a significant refactor or deprecation of envelope-related PDF viewing and signature functionality:

- `EnvelopeReceiverPage.razor`: A detailed PDF viewer with signature handling and interactive features.
- `EnvelopeReceiverPage_DxPdfViewer.razor`: A simplified PDF viewer using the `DxPdfViewer` component.
- `EnvelopeReceiverPage_DxReportViewer.razor`: A report viewer using the `DxReportViewer` component.
- `EnvelopeReceiverPage_embed.razor`: A minimalistic PDF viewer using the `<embed>` HTML tag.

These removals indicate a transition to a new approach for handling PDF viewing and related features or their complete deprecation.
2026-09-29 09:55:13 +02:00
19aeda5aef Add migration plan for WASM-based PDF editor component
A detailed migration plan has been added to replace the DevExpress PDF viewer in `EnvelopeSenderEditorPage.razor` with a new WebAssembly (WASM)-based PDF editor component.

The plan outlines the scope, constraints, and implementation strategy, ensuring feature parity, backend compatibility, and minimal disruption to the existing user experience. Key decisions include creating a reusable `SenderPdfImageEditor` component, using server-side PDF rasterization, and removing `esid` and `IMemoryCache`-based session persistence.

The document also defines testing and acceptance criteria to verify the migration's success, focusing on visual fidelity, functionality, and backend contract consistency.
2026-09-29 09:41:09 +02:00
940d129c11 Add read-only sharing and viewing functionality
Introduced the ability to share envelopes in read-only mode and view them via a dedicated page. Added `ShareEnvelopeReadOnlyAsync` in `AuthService.cs` to handle sharing logic. Updated `ReceiverPage.razor` with a "Share" button, popup UI, and validation for sharing inputs. Created `ReceiverReadOnlyPage.razor` for read-only document viewing with error handling and lifecycle management.

Added `EnvelopeKeyRedirectController.cs` to support legacy link redirection. Enhanced `ReadOnlyController.cs` to support envelope sharing with improved logging and `IMediator` integration. Improved state management, validation, and error handling across components. Performed general code cleanup and UI enhancements for better user experience.
2026-09-28 14:42:52 +02:00
44bbdbb835 Add logout functionality and signed status check
Introduced a logout feature to the `ReceiverPage` component:
- Added a logout button to the toolbar with `_isLoggingOut` state.
- Implemented `LogoutAsync` method to handle logout logic, including navigation and error handling.
- Injected `IMediator` and `ILogger<ReceiverPage>` for logging and MediatR functionality.
- Added a check in `OnInitializedAsync` to redirect users to `/envelope/signed-result` if the envelope is already signed.
- Updated namespaces to include MediatR and custom extensions.

These changes improve user experience and enhance the component's functionality.
2026-09-28 14:11:06 +02:00
223017006b Update launch URLs in launchSettings.json
Updated the `applicationUrl` property in the "https (Blazor UI)"
profile of `launchSettings.json`. Changed the HTTPS port from
8089 to 8088 and the HTTP port from 5132 to 5131. This ensures
the development server uses the new port configuration and
avoids potential conflicts with other services.
2026-09-28 13:59:44 +02:00
ad9250c7b1 Merge branch 'master' of http://git.dd:3000/AppStd/EnvelopeGenerator
# Conflicts:
#	EnvelopeGenerator.Server/EnvelopeGenerator.Server/Components/Pages/ReceiverSignedPage.razor
#	EnvelopeGenerator.Server/EnvelopeGenerator.Server/appsettings.json
2026-09-28 13:53:40 +02:00
5cc47ca9c7 Add signed result page and update navigation flow
Introduced a new `/envelope/signed-result` page to confirm
successful document signing with localized messages and
a visually appealing UI. Updated `ReceiverSignedPage.razor`
to redirect users to the new page after signing.

Enhanced styles in `app.css` for the signed result page,
including responsive layout, theme-specific designs, and
typography improvements. Added support for localization
using `IStringLocalizer` with fallback strings in German.

Improves user experience by providing clear feedback and
a polished confirmation page after signing.
2026-09-28 13:46:48 +02:00
156988403a Add envelope receiver access code request feature
Introduced a feature to allow envelope receivers to request an
access code via email on the login page.

- Added `OnInitializedAsync` in `LoginReceiverPage.razor` to
  trigger access code requests using `AuthService`.
- Implemented `RequestEnvelopeReceiverAccessCodeAsync` in
  `AuthService.cs` to handle API calls for access code requests.
- Added a new endpoint in `AuthController` to process access
  code requests, including validation, history recording, and
  email dispatching.
- Registered `IEnvelopeMailService` and `AddDispatcher` in
  `Program.cs` to support email dispatch and database operations.
- Refactored `AuthController` constructor for readability and
  added necessary `using` directives.
- Improved logging and error handling for better traceability
  and robustness.

These changes ensure a seamless and secure process for envelope
receivers to request access codes, enhancing the user experience.
2026-09-28 13:35:32 +02:00
e960c02cd4 Add AccessCode feature to EnvelopeSenderPage UI
Introduced a new `AccessCode` property in `EnvelopeReceiverDto` to support displaying and copying access codes in the envelope sender page.

- Updated `EnvelopeReceiverDto` to include the nullable `AccessCode` property.
- Enhanced `EnvelopeSenderPage.razor` to display the `AccessCode` for each receiver, with a copy-to-clipboard button.
- Implemented `CopyAccessCodeAsync` for clipboard functionality using JavaScript interop.
- Styled the `AccessCode` section in `sender-page.css` for a responsive and user-friendly design.
- Improved error handling for clipboard operations with console warnings.

These changes improve the user experience by allowing senders to view and copy access codes for receivers.
2026-09-28 12:47:37 +02:00
a0e2492757 Add email queuing for envelope invitation process
Introduced functionality to queue and send invitation emails to envelope receivers. Added new dependencies and private properties to support email handling, including repositories for `EmailTemplate` and `EmailOut`, as well as configuration services for `MailParams` and `DispatcherParams`.

Added the `QueueInvitationEmailsAsync` method to generate personalized email content using placeholders and store outgoing emails in the `EmailOut` repository. Introduced a helper method `ReplacePlaceHolders` for placeholder substitution in email templates.

Updated the `Handle` method to invoke `QueueInvitationEmailsAsync` after setting the envelope status to `EnvelopeQueued`. These changes ensure a modular and reusable email queuing process.
2026-09-28 12:35:51 +02:00
380a5f4c0c Add signature submission feature and pipeline updates
Introduced a new feature for submitting signatures, including
backend and frontend changes. Added `SignatureSubmitDto` and
`SubmitAsync` in `SignatureService` to handle signature data
submission. Implemented a new `Submit` endpoint in
`SignatureController` to validate and process signature payloads.

Updated pipeline behaviors to include `SaveSignatureBehavior`
and adjusted the execution order. Enhanced query logic in
`EnvelopeReceiverResolutionBehavior` and `SaveSignatureBehavior`
to align with the updated data model.

Refactored `EGDbContext` to improve trigger handling and logging.
Updated `ReceiverSignedPage.razor` to integrate `SignatureService`
and handle submission errors gracefully. Adjusted application
URLs in `launchSettings.json` and added new database triggers
in `appsettings.json`.
2026-09-28 12:21:44 +02:00
28f8b829e3 Refactor: Unify grid logic for active/completed tabs
Replaced separate `DxGrid` components for "active" and "completed" tabs with a single unified grid. Introduced dynamic properties (`CurrentGridData` and `CurrentGridKey`) to handle data and layout switching based on the active tab.

Refactored layout loading/saving methods into shared versions (`OnGridLayoutAutoLoading` and `OnGridLayoutAutoSaving`) using dynamic keys. Simplified export logic to use the unified grid instance.

Removed redundant code, templates, and private fields specific to the "completed" tab. Updated status filter logic to dynamically fetch data. Improved maintainability by consolidating logic and reducing duplication.
2026-09-28 12:06:35 +02:00
d00c459124 Optimize query with eager loading for related entities
Added `Include` statements to the LINQ query in the
`ReadEnvelopeQueryHandler` class to eagerly load related
entities (`Type`, `EnvelopeReceivers` with `Receiver`, and
`Histories`). This change reduces lazy-loading overhead and
ensures all necessary data is retrieved in a single query,
improving performance.
2026-09-28 11:57:41 +02:00
287c14e114 Refactor signed status logic in EnvelopeSenderPage
Introduce `GetSignedReceiverCount` and `IsReceiverSigned` helper methods to centralize and improve the logic for determining the signed status of receivers. Replace inline logic with these methods for better encapsulation and reusability.

Update UI rendering logic to dynamically determine receiver badge states based on the new methods. These changes enhance code maintainability, reduce duplication, and ensure consistent behavior across the application.
2026-09-28 11:55:31 +02:00
212ba9d569 Enhance grid filtering and sorting functionality
Updated `DxGrid` components for `_activeEnvelopes` and `_completedEnvelopes`:
- Disabled `ShowFilterRow` and added `FilterMenuButtonDisplayMode` to always display filter menu buttons.
- Enabled sorting for the `Id` column with descending order and added a display format.
- Added `EditSettings` to the `Status` column with a `DxComboBoxSettings` for filtering, using the new `GetStatusFilterItems` method.

Introduced `GetStatusFilterItems` to generate distinct status filter items and added a private `StatusFilterItem` record type for encapsulating status values and labels.

Made minor adjustments to grid layout auto-saving logic for consistency.
2026-09-28 11:40:22 +02:00
735f072dd1 Add localization support for multi-language UI
This commit introduces comprehensive localization support for the application, enabling dynamic language switching and localized UI elements. Key changes include:

- Added `.resx` files for German (de-DE), English (en-US), and French (fr-FR).
- Updated Razor components to use `IStringLocalizer` for dynamic string localization.
- Introduced the `L(string key, string fallback)` helper method for consistent localized string retrieval with fallback support.
- Implemented dynamic culture initialization in `Program.cs` via an API call to `/api/Localization/lang`.
- Localized error pages, grid columns, tabs, popups, validation messages, and tooltips.
- Dynamically generated privacy policy links based on the current culture.
- Improved user experience with fully localized loading states, error handling, and UI labels.

These changes enhance accessibility and usability, making the application more user-friendly for a global audience.
2026-09-28 10:46:06 +02:00
0f53d0509e remove depr API 2026-09-28 09:51:51 +02:00
48cfd3f416 Add French localization resource file
Added `Model.fr-FR.resx` to support French localization.
Defined XML schema for the resource file structure, including
metadata, assembly, and data elements. Added localized
French strings for various application messages, ensuring
proper display when the locale is set to `fr-FR`.
2026-09-28 09:40:56 +02:00
0d2e43b2d6 Add multilingual support with language selector
Introduced a `LanguageControls` component for switching between
languages (`de-DE`, `en-US`, `fr-FR`) and integrated a new
`LocalizationService` to manage language preferences. Updated
`LocalizationController` to handle language persistence via
cookies and support multiple cultures. Enhanced the footer
layout to include the language selector alongside theme
controls.

Added `flag-icons` library for displaying country flags and
configured request localization in `Program.cs`. Styled the
language selector dropdown and improved sender page visuals
with updated CSS. Defaulted language handling to `de-DE` for
fallback scenarios. Cleaned up redundant code in
`LocalizationController`.
2026-09-28 09:19:46 +02:00
5aaa37b8ec Default localization to "de-DE" and refactor resources
Added a new `Model` class in the `EnvelopeGenerator.Domain.Resources` namespace as a marker for `IStringLocalizer` resource lookup. Updated `EnvelopeSenderPage.razor` to use the new `Model` class for localization.

Modified `LocalizationController` to default to "de-DE" when no culture is specified or found. Refactored localization setup in `Program.cs` to use a fixed default culture of "de-DE" and persist culture via cookies. Removed dynamic culture setup and `QueryStringRequestCultureProvider`.

Performed minor cleanup in `LocalizationController` and `Program.cs` to align with the new localization approach.
2026-09-28 08:46:11 +02:00
051a8ce211 Add history display and enhance status handling
Introduced a "Historie" section in `EnvelopeSenderPage.razor` to display envelope history with status badges, actor info, and timestamps. Updated `ReadEnvelopeQueryHandler` to include histories in queries.

Refactored `GetStatusInfo` to use localized labels and expanded supported statuses. Added helper methods for history processing and display.

Enhanced UI with new styles for the history section in `sender-page.css` and improved responsive design. Added dependency injections for localization and history handling.
2026-09-28 08:39:39 +02:00
519a8bf3f9 Refactor sender resolution and add history logging
Refactored sender email resolution into a new private async
method `ResolveSenderReferenceAsync` to improve code
reusability and readability. Replaced inline logic for
resolving sender references with this method.

Added a `CreateHistoryCommand` to log envelope creation
events, including envelope ID, sender reference, and status
(`EnvelopeCreated`). This enhances auditability and ensures
consistent event logging.
2026-09-27 20:20:01 +02:00
a7d0b40e39 Refactor DocReceiverElement handling and add draft workflow
Removed `IDocReceiverElementWriter` interface and its implementation, replacing it with repository-based operations (`IRepository<DocReceiverElement>`). Updated `CreateEnvelopeCommandHandler` to use repository methods for deleting and creating `DocReceiverElement` records.

Cleaned up dependency injection by removing `IDocReceiverElementWriter` registration. Added new database triggers for `DocReceiverElement` in `appsettings.json` and `appsettings.Database.json`.

Introduced `SENDER_SAVE_EDIT_MIGRATION_PLAN.md` to outline the migration of the sender save/edit workflow to Blazor Server. This includes splitting save and send actions, supporting draft persistence, and enabling draft editing from the dashboard.

Removed unused namespaces and performed code cleanup. Updated testing strategies to ensure correctness and prevent regressions.
2026-09-27 19:56:02 +02:00
3dc9a180d3 docs(agents): align sender workflow endpoint guidance with upsert API
- replace outdated draft/update/send endpoint list with current POST /api/Envelope workflow contract
2026-09-26 13:11:50 +02:00
4dba965c75 feat(sender): route editor save and send through unified envelope upsert API
- expose POST /api/Envelope for sender-authenticated create/update/send operations

- replace client draft-specific call with EnvelopeService.UpsertAsync(CreateEnvelopeCommand)

- update sender editor save/send flow to persist draft first and send by envelope id

- allow editor hydration without document by making sender document fetch nullable
2026-09-26 13:11:44 +02:00
4d2538cc2c refactor(envelope): consolidate draft flow into upsert command
- extend CreateEnvelopeCommand to support draft updates and send intent

- merge draft save logic into CreateEnvelopeCommandHandler and remove duplicate handler

- add DocReceiverElement writer abstraction and infrastructure registration for SQL-based field upserts
2026-09-26 13:11:28 +02:00
d201eacc97 Unify Blazor frontend and API into EnvelopeGenerator.Server
The `EnvelopeGenerator.Server` project now serves as the unified runtime host for both the Blazor frontend and Web API backend, deprecating the `EnvelopeGenerator.API` project. All API endpoints have been merged into `EnvelopeGenerator.Server`.

Key changes:
- Updated `EnvelopeGenerator.Server` to include API controllers.
- Deprecated `EnvelopeGenerator.API` and removed it from the active solution.
- Simplified development workflow to run only `EnvelopeGenerator.Server`.
- Updated `appsettings.json` to consolidate configuration settings.
- Adjusted documentation to reflect the new unified architecture.
- Updated manual testing steps and environment variable references.
- Migrated legacy VB.NET app references to the new architecture.

These changes simplify development, deployment, and testing workflows while consolidating the solution architecture.
2026-09-25 14:26:16 +02:00
9daa671655 chore(solution): remove API project entry from solution 2026-09-25 14:22:08 +02:00
3fbf011fe3 refactor(envelope): update delete handler with explicit field update 2026-09-25 14:21:56 +02:00
a815705fa4 feat(sender): support draft editing flow in sender pages 2026-09-25 14:21:23 +02:00
6e44d0d4b5 feat(sender): add envelope draft save command and endpoint 2026-09-25 14:21:08 +02:00
42a853cb0d refactor(API): removed because migrated to the server-application 2026-09-25 12:56:46 +02:00
b46c3c8ebe feat(sender): add envelope delete popup and client call 2026-09-25 11:31:53 +02:00
9f712a6b8c test: cover delete envelope command scenarios 2026-09-25 11:31:47 +02:00
4449743105 feat: add envelope delete command and API endpoint 2026-09-25 11:31:40 +02:00
435be882c9 Complete SF-74 implementation and fix PDF issues
Updated SF-74-PLAN.md to document completed phases (6a–6e), including theme selection, dark mode, grid persistence, and logo placement. Highlighted pending tasks for phase 6f and clarified open questions.

Revised MIGRATION_PLAN.md with deep analysis findings and detailed steps for migrating `EnvelopeGenerator.Service_legacy` to a modern C# Worker Service. Addressed connection string handling, Quartz version mismatch, and TempFiles architecture.

Removed outdated markdown files from EnvelopeGenerator.sln and updated the solution structure for consistency.

Fixed incorrect "Read and confirmed" label in Signature Certificate reports for "Read and Sign" envelopes. Added branching logic in `ReportItem.vb` and updated resource files.

Resolved signature field formatting issues in finalized PDFs based on WISAG feedback. Added truncation logic in `PDFBurner.vb` and updated `PDFBurnerParams.vb` with per-field character limits.
2026-09-25 09:17:50 +02:00
09257d7766 Refactor sender title and improve responsiveness
Refactored the sender title in `EnvelopeSenderPage.razor` to use a structured layout with accessibility improvements. Introduced `.sender-title-main` and `.sender-title-sub` classes for better styling and responsiveness. Updated `.sender-page.css` to enhance layout flexibility, reduce gaps, and improve scaling for logos and titles.

Added media queries for better responsiveness on smaller screens, including adjustments to `.sender-title-section` and logo dimensions. Introduced theme-specific overrides for "purple" and "blazing-dark" themes to ensure consistent styling.

Minor adjustments were made to padding, margins, and font sizes to improve overall layout and visual hierarchy.
2026-09-25 00:30:03 +02:00
16fa4b460f Remove "Grid persistence hint" feature
The "Grid persistence hint" feature was removed from `EnvelopeSenderPage.razor`, including its associated HTML `<div>` and SVG icon.

Corresponding styles for `.sf-grid-persistence-hint` were removed from `sender-page.css`, including display, alignment, padding, font size, color, background, and border.

Adjusted the padding for `.sender-grid-wrapper` in `sender-page.css` to account for the removal of the hint banner, setting it to `1rem 2rem 2rem`.
2026-09-25 00:16:36 +02:00
f4aae8c787 Improve logo rendering and page responsiveness
Updated `EnvelopeSenderPage.razor` to enhance customer logo rendering with dynamic height and class styling. Refined the "Nur ausgewählte" checkbox label for better styling.

Modified `sender-page.css` to improve layout and responsiveness:
- Added styles for `.sender-customer-logo-area` and `.signflow-company-logo` for proper scaling and alignment.
- Removed `.sender-export-label` styles and introduced `.sender-export-selected-text` for improved checkbox label design.
- Adjusted media queries for better layout on screens up to 1320px and 768px.

Enhanced overall responsiveness and accessibility of the sender page.
2026-09-25 00:14:08 +02:00
ed1468194d Add export functionality to /sender Dashboard (Phase 6e)
Finalized the implementation of the export feature for the
`EnvelopeSenderPage.razor` in the Blazor WASM Client. The feature
supports exporting grid data in XLSX, CSV, and PDF formats, with
an option to export only selected rows.

- Updated `SF-74-PLAN.6e.md` to reflect the completed Phase 6e.
- Integrated a compact UI for export controls in the toolbar:
  - Dropdown for format selection (XLSX, CSV, PDF).
  - Checkbox for "Only selected rows" option.
  - Single `Exportieren` button to trigger the export.
- Implemented `ExportSelectedFormatAsync` to handle export logic:
  - Supported formats: XLSX, CSV, PDF.
  - Added options for `ExportSelectedRowsOnly` and grouping.
- Replaced placeholder `ExportToExcelAsync` with finalized logic.
- Documented known limitations (e.g., template columns not exported).
- Updated project status table to mark Phase 6e as complete.

This commit ensures a streamlined and user-friendly export UX,
consistent with the existing design, while addressing known
DevExpress limitations.
2026-09-24 18:38:11 +02:00
f9f94c900e Implement export functionality with loading spinner
Replaced placeholder export method with a fully implemented
`ExportSelectedFormatAsync` method to support exporting grid
data in CSV, PDF, or XLSX formats. Added a loading spinner
to the export button to indicate progress during export
operations. Updated the `IsExportDisabled` method to account
for the new `_isExporting` state.

Disabled exporting for "Status" and "EnvelopeReceivers"
columns by setting their `ExportEnabled` property to `false`.
Made minor grid layout adjustments, including adding the
`KeyFieldName` property for proper grid behavior.
2026-09-24 18:32:03 +02:00
18df6221c9 Add export feature UI to EnvelopeSenderPage (placeholder)
Introduced a new export feature to the EnvelopeSenderPage, including a dropdown for selecting export formats (XLSX, CSV, PDF) and a checkbox to export only selected rows. Added a placeholder export button ("Exportieren") that is currently disabled, with export logic to be implemented in a future phase.

Added private fields `_selectedExportFormat` and `_exportSelectedRowsOnly` to manage the export state. Implemented `IsExportDisabled()` to control button availability and a placeholder method `ExportSelectedFormatPlaceholderAsync()`.

Updated `sender-page.css` to style the export feature, including the export button, dropdown, and checkbox. Added theme-specific styles for "purple" and "blazing-dark" themes. Adjusted toolbar element order to position the export group before the logout button.
2026-09-24 18:11:37 +02:00
b15f95755c Add multi-format export UI to EnvelopeSenderPage
Implemented the first phase of a planned multi-format export feature for the `/sender` dashboard. Added a compact export block to the right action bar in `EnvelopeSenderPage.razor`, including a format dropdown (XLSX, CSV, PDF), a "Selected Rows Only" toggle, and an "Export" button.

The UI is fully integrated with responsive design and theme consistency across `blazing-berry`, `blazing-dark`, and `purple`. No export logic is implemented yet; placeholder handlers are in place for the export button. Styles were added to `wwwroot/css/sender-page.css` to ensure alignment with the existing design language and mobile usability.

This commit completes Phase 1 of the implementation plan. Phase 2 will add the actual export functionality.
2026-09-24 18:07:05 +02:00
4be9e38a9e Mark Phase 6d as complete; add PDF preview details
Updated the "Letzte Aktualisierung" date in SF-74-PLAN.md to
reflect the latest progress. Added requirement 6d for a PDF
Preview Popup using `DxPdfViewer` and marked it as completed.
Provided additional context for Phase 6d, explaining the
read-only behavior of envelopes after sending and the technical
approach for implementing the PDF preview functionality.
2026-09-24 16:28:16 +02:00
11c8886d6f chore(release): bump version to 1.2.0-beta
Reflects the Scalar API reference and sender PDF preview popup
refactor shipped in this cycle.
2026-09-24 12:02:30 +02:00
1ad673904a refactor(sender): overhaul PDF preview popup layout and styling
Replace inline styles and magic-number height calculations with a proper
BEM CSS class system. Attach HeaderCssClass and BodyCssClass to DxPopup
so DevExpress DOM targets can be styled reliably without deep selector
hacks.

Layout changes:
- Remove hardcoded calc(90vh - 114px) height from viewer wrapper; use
  flex layout so the body fills available space naturally
- Change overflow on DxPdfViewer wrapper from hidden to auto to allow
  horizontal scrolling at high zoom levels
- Center PDF pages horizontally inside dxbrv-document-surface via flex

New CSS classes (BEM):
- .sender-preview-popup           root shell, accent top border
- .sender-preview-popup__header   styled header via HeaderCssClass
- .sender-preview-popup__body     flex column body via BodyCssClass
- .sender-preview-viewer-wrap     padding wrapper for the PDF viewer
- .sender-preview-pdf-viewer      viewer shell with rounded corners / shadow
- .sender-preview-state-wrap      centered container for loading/error states
- .sender-preview-error-card      error message card with left accent border
- .sender-preview-error-icon      circular icon wrapper
- .sender-preview-error-title/msg typography helpers

Theme support:
- Purple, Blazing Dark variants via data-sf-theme attribute overrides

Custom scrollbars:
- WebKit scrollbar styling scoped to .sender-preview-popup__body

Inline style cleanup in EnvelopeSenderPage.razor:
- Replace all ad-hoc style= attributes with the new CSS classes above
2026-09-24 12:02:22 +02:00
32a9d008a4 feat(api): add Scalar API reference with independent toggle
Decouple Scalar from Swagger so each can be enabled/disabled separately
via the UseScalar config flag. When Scalar is enabled without Swagger,
the bare OpenAPI JSON endpoint is still registered so Scalar has a
spec to load from.

- Add UseScalar flag to appsettings.json (default: true)
- Resolve useScalar from config in Program.cs
- Map Scalar only when useScalar is true
- Ensure UseSwagger() is called if Scalar needs it but Swagger UI is off
2026-09-24 12:02:08 +02:00
454dd0b6cc feat: implement PDF preview popup on sender dashboard
Replace the placeholder preview popup with a fully functional DxPdfViewer
popup:

- Popup resized to 80vw × 90vh with drag, resize and background shading.
- OpenPreviewAsync: unmounts viewer, fetches PDF bytes via
  GetDocumentOfSenderAsync, then mounts DxPdfViewer only after bytes are
  ready (prevents DevExpress ProcessDocumentChangedAsync NRE).
- Loading spinner shown while fetching; typed error panel shown on
  NotFoundException or general failure.
- OnPreviewClosed: unmounts viewer before nulling byte array so DevExpress
  async render never touches a disposed reference.
- Inject ReceiverPageDataService, EnvelopeAuthService and ILogger.
2026-09-23 15:33:17 +02:00
aa2a31e993 style: add global user-select:none and PDF preview popup styles
app.css: Disable text/image selection globally; re-enable for
input/textarea/[contenteditable] so typing and pasting still work.

sender-page.css: Add .sender-preview-popup, .sender-preview-viewer-wrap
and .sender-preview-pdf-viewer rules. DxPdfViewer requires an explicit
pixel-height parent — percentage heights resolve to 0 inside popup body
flex containers and cause DevExpress NullReferenceExceptions.
2026-09-23 15:33:08 +02:00
ecc509919c feat: add GetDocumentOfSenderAsync to EnvelopeReceiverPageDataService
Loads the PDF bytes for a given envelope on behalf of the authenticated
sender (uses SenderId() claim extension). Throws NotFoundException when
no document data is available so callers can surface a typed error.
2026-09-23 15:32:58 +02:00
f3751274b1 feat: make Swagger/Scalar available outside Development via UseSwagger flag
Previously Swagger UI was only registered in Development. Add a
'UseSwagger' config key so staging/production environments can opt in
without code changes.
2026-09-23 15:32:51 +02:00
adc231f10e fix: resolve sender identity in Blazor Server SignalR circuits
JwtBearer middleware does not run for WebSocket upgrade requests, so
HttpContext.User is anonymous inside Blazor Server components. Fall back
to manual cookie parsing + token validation (same pattern used by
EnvelopeReceiverAuthorizationService). Cache the resulting principal on
HttpContext.User for subsequent calls within the same circuit.

Also inject IOptionsMonitor<JwtBearerOptions> and IOptions<AuthTokenKeys>
to support the validation path.
2026-09-23 15:32:44 +02:00
999fac7dac feat: add SenderId() extension to ClaimsPrincipal
Extracts the authenticated sender's integer ID from the 'sub' /
NameIdentifier claim. Mirrors the existing ReceiverId() helper and is
required by GetDocumentOfSenderAsync.
2026-09-23 15:32:36 +02:00
86758bb127 feat(application): add UserId filter to ReadDocumentQuery
Adds an optional UserId parameter to ReadDocumentQuery so callers
can scope document lookups to envelopes owned by a specific sender.
Also adds AsNoTracking() to the base query for read-only scenarios.
2026-09-23 15:28:55 +02:00
498d59a4ce Rename GetDocumentAsync to GetDocumentOfReceiverAsync
Updated the method name in EnvelopeReceiverPageDataService
to clarify its purpose of retrieving documents for a receiver.
Refactored all references in Razor pages (ReceiverPage.razor,
ReceiverSignedPage.razor, EnvelopeReceiverPage.razor) to use
the new method name. No changes were made to the method's
logic or functionality. This improves code clarity and consistency.
2026-09-23 11:40:52 +02:00
ce430c1b80 refactor(ReceiverUI): remove deprecated app 2026-09-22 16:54:46 +02:00
eb219291de Migrate WebUI to EnvelopeGenerator.Server
Updated documentation to reflect the migration from WebUI to EnvelopeGenerator.Server, including a new "Documentation Catalog" in `AGENTS.md`. Adjusted route mappings, render modes, and directory structure for Blazor components.

Revised coordinate system documentation and CSS for status colors to align with the new architecture. Updated YARP proxy and PDF.js configuration paths.

Migrated `MIGRATION_PLAN.md` to modernize `EnvelopeGenerator.Service` with C# Worker Service equivalents. Updated `FORM_APPLICATION_CONTEXT.md` and `RECEIVER_PDF_VIEWER_CONTEXT.md` to reflect the new architecture and planned viewer integration.

Fixed German translation issues in `fix-report-label-read-and-confirmed.md`. Standardized formatting and terminology across all files.
2026-09-22 16:17:09 +02:00
80733ab1f7 Switch rendering mode to InteractiveServer
Updated the `@rendermode` directive in `EnvelopeSenderPage.razor`
to replace `InteractiveWebAssemblyRenderMode` (with `prerender: false`)
with `InteractiveServer`. This change transitions the rendering
approach from WebAssembly-based interactive mode to server-side
interactive mode.
2026-09-22 12:03:30 +02:00
3d9153da23 move(sender): migrate EnvelopeSenderPage to Server project for SSR 2026-09-22 12:03:09 +02:00
a0bfd405f0 Simplify row selection logic in EnvelopeSenderPage
Refactored row selection logic to leverage DevExpress's native
`@bind-SelectedDataItem` for automatic updates to the
`_selectedEnvelope` variable. Added `AllowSelectRowByClick`
to enable row selection via single click. Updated event
handlers for layout auto-loading and auto-saving to use
specific methods for different grids. Simplified `OnRowClick`
method by removing redundant manual selection logic and
retaining toggle behavior for deselecting the same row.
Updated comments to reflect these changes.
2026-09-21 17:12:35 +02:00
ec3a15a467 Add envelope preview feature with popup and row events
Enhanced the UI with a "Vorschau" (Preview) button to display
PDF previews of selected envelopes. Introduced a `DxPopup`
to show envelope details, including ID and title, with a
"Schließen" (Close) button.

Replaced `SelectedDataItemChanged` with `RowClick` and
`RowDoubleClick` events for improved row selection.
Single-click toggles row selection, while double-click opens
the preview popup. Added `_previewVisible` to manage popup
visibility and implemented `ShowPreview` for button-triggered
previews. Removed `OnSelectedEnvelopeChanged` as it is now
handled by the new row events. Updated grid layout persistence
to align with the new event structure.
2026-09-21 17:07:20 +02:00
80f08b393c Update SF-74-PLAN.md: Complete phases 6a–6c
Reflect completion of phases 6a–6c in the project plan:
- Marked phases 6a (signFLOW logo), 6b (customer logo placeholder),
  and 6c (grid columns + persistence hint) as completed.
- Updated dependency rule to reflect the change from
  `EnvelopeGenerator.Server.Client` to `Blazor WASM Client`.
- Reiterated key decisions, including the use of `TBDD_CACHE`,
  `SenderOrReceiver` policy, and `signflow.theme` in localStorage.
- Noted DevExpress 25.2.3 lacks `.dark.`-suffix files.
2026-09-21 16:52:09 +02:00
b22d57d7da Add SF-74-PLAN.md to SolutionItems in EnvelopeGenerator
A new file, `SF-74-PLAN.md`, was added to the `EnvelopeGenerator.Server` project under the `SolutionItems` section in the solution file (`EnvelopeGenerator.sln`). This change ensures the file is tracked as part of the solution.
2026-09-21 15:05:23 +02:00
01cd8b2607 Improve publish script with dynamic versioning
Added dynamic version resolution from the `<Version>` tag in the
`.csproj` file, enabling versioned output directories. Updated
`setlocal` to use `enabledelayedexpansion` for better variable
handling. Enhanced logging of build parameters and streamlined
the "Publishing" step by removing redundant logs.

Improved output verification with additional checks for critical
files (`hostfxr.dll`, `coreclr.dll`, etc.), logging failures if
files are missing. Updated success messages to include the
resolved version and generalized next-step instructions.
2026-09-21 14:17:49 +02:00
310d2efc6e Update project version to 1.1.0-beta
Updated versioning details in `EnvelopeGenerator.Server.csproj`:
- Changed `<Version>` from `1.0.1-beta` to `1.1.0-beta`.
- Updated `<FileVersion>` from `1.0.1.0` to `1.1.0.0`.
- Updated `<AssemblyVersion>` from `1.0.1.0` to `1.1.0.0`.

This minor version update reflects new features or improvements
while maintaining backward compatibility.
2026-09-21 13:55:42 +02:00
e5402bafc9 Refactor and cleanup EnvelopeSender components
Refactored code for improved readability, consistency, and maintainability. Removed the WASM boot loading overlay and its associated styles/scripts. Updated SVG paths to use self-closing tags for HTML compliance. Reformatted conditional blocks, state management, and session data logic. Enhanced error handling, spinner visibility, and empty state displays.

Removed redundant comments and unused code. Added `envelope-editor.js` for new functionality. Cleaned up CSS and JavaScript related to the removed WASM overlay. Improved grid layout persistence and signature field logic. Adjusted localization strings for better alignment with German translations.
2026-09-21 13:05:59 +02:00
f517ab4cfb Enhance grid UI and refactor boot overlay logic
- Updated `MappingProfile.cs` to add detailed mapping for `Envelope` to `EnvelopeDto`, including `EnvelopeTypeTitle`.
- Added a grid persistence hint banner in `EnvelopeSenderPage.razor` with an SVG icon and German message.
- Introduced three new columns (`EnvelopeTypeTitle`, `AddedWhen`, `ChangedWhen`) to the grid in `EnvelopeSenderPage.razor`.
- Refactored boot overlay removal logic in `App.razor` into a reusable function `sfRemoveBootOverlay`.
- Simplified event handling for overlay removal and fallback logic in `App.razor`.
- Adjusted `.sender-grid-wrapper` padding in `sender-page.css` and added styles for the new grid persistence hint banner.
2026-09-21 12:03:27 +02:00
6d63f9db2b Update Default connection string in appsettings.json
The `ConnectionStrings.Default` value in `appsettings.json` was updated.
The connection string now points to the testing environment
(`SDD-VHP04-SQL19\DD_TESTING01`) instead of the development environment
(`SDD-VMP04-SQL17\DD_DEVELOP01`). The password was also updated to
`123456789dD!` to reflect the new environment credentials.
2026-09-21 11:07:50 +02:00
46c7c31e03 Add customer logo area to action bar
Introduced a customer logo section in `EnvelopeSenderPage.razor` that dynamically displays a customer logo or a placeholder when no logo is configured. The logo is fetched from the `CustomImagesService` and styled using new CSS classes in `sender-page.css`.

- Added `_companyLogo` property to store customer logo configuration.
- Updated `CustomImagesService` to fetch both app and customer logos.
- Modified `appsettings.json` to include logo paths and styles.
- Enhanced CSS for a three-section action bar layout.
- Added placeholder styling for missing customer logos.
- Updated initialization logic to load logos dynamically.
- Documented implementation details in `SF-74-PLAN.md`.

The implementation is modular and deployment-specific, allowing logo paths and styles to be updated via configuration without code changes.
2026-09-06 10:50:05 +02:00
8f6db435bb SF-74: Client UI — IndexPage + EnvelopeSenderPage logo integration (signFLOW + company logo, branding layout) 2026-09-04 01:07:34 +02:00
9bf7676464 SF-74: Client — CustomImagesService (in-memory cache, GET /api/CustomImages), DI registration 2026-09-04 01:07:27 +02:00
d9556c18ca SF-74: Server wwwroot — logo images (DD_signFLOW_LOGO.png, digital_data.svg), app.css logo CSS classes 2026-09-04 01:07:16 +02:00
5b7157f422 SF-74: Server — CustomImagesController (GET /api/CustomImages), DI registration, appsettings logo config, App.razor 2026-09-04 01:07:04 +02:00
119ee3ec16 SF-74: Models — CustomImagesOptions (Server, Options Pattern), CustomImagesDto (Client) 2026-09-04 01:06:52 +02:00
b536b31a4b SF-74: CSS — app.css theme/dark-mode additions, envelope-viewer.css, sender-page.css new stylesheets 2026-09-03 15:40:20 +02:00
a80efee075 SF-74: ThemeService + signflow-theme.js — JS interop fixes and dark mode init improvements 2026-09-03 15:40:14 +02:00
86d6daafbd SF-74: EnvelopeSenderPage — ThemeService/UserPreferencesService inject, LoadFromServerAsync, grid layout persistence (LayoutAutoLoading/Saving) 2026-09-03 14:58:31 +02:00
aefa1a8883 SF-74: Client UI — MainLayout HeadContent+footer theme controls, ThemeControls component, app.css sf-footer styles 2026-09-03 14:58:19 +02:00
b3e7f77165 SF-74: Server — add signflow-theme.js (localStorage/system dark mode JS interop) 2026-09-03 14:58:07 +02:00
bac9bfd036 SF-74: Client Services — ThemeService (debounce, SaveError, DisplayName), UserPreferencesService (1h cache) 2026-09-03 14:57:55 +02:00
6effb8a0d6 SF-74: Server — UserPreferencesController, claim extensions, DI registration, App.razor 2026-09-03 14:57:44 +02:00
a9ff0d88f2 SF-74: Infrastructure — UserPreferencesService (IDistributedCache, no try-catch) 2026-09-03 14:57:32 +02:00
0fdfe991ea SF-74: Domain/Application — UserPreferences DTO, interface, Sender role constant 2026-09-03 14:57:21 +02:00
7fc2c14f9c Add dynamic theme and dark mode functionality
Introduced `ThemeService` to manage themes and dark mode, with a new footer UI for theme selection and toggling dark mode. Added dynamic CSS loading and Bootstrap theme support. Registered `UserPreferencesService` and `ThemeService` for SSR compatibility. Removed hardcoded DevExpress theme links.

Fixed German localization issues in `LoginReceiverPage` and `LoginSenderPage`. Updated `app.css` with styles for the new footer controls. Simplified font loading in `MainLayout.razor`.
2026-09-03 09:24:13 +02:00
5098deedd1 Add ThemeService and UserPreferencesService
Introduce ThemeService to manage themes and dark mode state,
including initialization, persistence, and server sync.

Add UserPreferencesService to handle API communication for
retrieving and saving user preferences with in-memory caching
to reduce network overhead.

Register both services in the DI container in Program.cs.

Enhance ThemeService with detailed documentation and introduce
ThemeOption record for selectable themes. Improve server-side
caching in UserPreferencesService for consistency and efficiency.
2026-09-03 01:18:01 +02:00
2e634361c5 Add user preferences management via distributed cache
Introduced `UserPreferencesDto` to represent UI preferences,
including theme, dark mode, and grid layouts. Added the
`IUserPreferencesService` interface and implemented it in
`UserPreferencesService` to manage preferences stored in
the distributed cache (`dbo.TBDD_CACHE`).

Registered `UserPreferencesService` in dependency injection
and added a NuGet reference for `Microsoft.Extensions.Caching.Abstractions`.

Created `UserPreferencesController` with `GET` and `PUT`
endpoints to retrieve and save user preferences. Validated
theme names in the `PUT` endpoint.

Updated `Program.cs` to ensure proper service registration.
2026-09-02 12:35:19 +02:00
60ebebc0d8 Add widget annotation and update stroke color
Updated `createAnnotations` to initialize a `PSPDFKit.Annotations.WidgetAnnotation` object, enabling the creation of widget annotations.

Modified `ctx.strokeStyle` in `createAnnotationFrameBlob` to use 'darkblue' instead of 'darkgray', improving the visual appearance of the annotation frame.
2026-08-27 14:46:32 +02:00
c2d8b6531c Target single framework and add publish profile
Updated `EnvelopeGenerator.Service.csproj` to target a single framework (`net4.6.2`) by replacing `<TargetFrameworks>` with `<TargetFramework>`.

Added `DefaultProfile.pubxml` to define a publish profile with settings for release configuration, platform, and file system-based publishing.
2026-08-27 12:44:11 +02:00
665266457f Migrate legacy VB.NET service to modern C# Worker
Migrated the legacy VB.NET Windows Service to a modern C# Worker Service targeting .NET Framework 4.6.2. Key changes include:

- Added `Microsoft.Extensions.Hosting.WindowsServices`, `NLog`, and `Quartz` for logging and scheduling.
- Introduced `LogConfigFactory` to bridge `ServiceConfig` and `LogConfig` for Quartz jobs.
- Rewrote `Worker` to handle the full service lifecycle, including database connection, temp file management, and scheduler initialization.
- Implemented `SchedulerFinishEnvelope` and `SchedulerEnvelopeTaskApi` for Quartz job scheduling.
- Created `TempFiles` for service-level temp file management.
- Configured `NLog` for file and console logging.
- Updated `appsettings.json` for connection strings and service-specific configuration.
- Replaced legacy `ProjectInstaller` with `AddWindowsService()` for SCM integration.
2026-08-27 11:39:49 +02:00
14d581acac Add EnvelopeGenerator.Service as a .NET Worker Service
Introduced a new project `EnvelopeGenerator.Service` to the solution as a .NET Worker Service targeting the `net4.6.2` framework.

- Configured the project with nullable reference types, implicit usings, and `LangVersion` set to `latest`.
- Added a `Worker` class that logs periodic messages while running.
- Updated `Program.cs` to configure and run the Worker Service.
- Added `launchSettings.json` for development environment configuration.
- Added `appsettings.json` and `appsettings.Development.json` for logging configuration.
- Included the project in `EnvelopeGenerator.sln` with appropriate build configurations and hierarchy updates.
2026-08-27 10:36:55 +02:00
eb533a1dc2 Replace EnvelopeGenerator.Service with Service_legacy
The project `EnvelopeGenerator.Service` has been removed from the solution and replaced with a new project, `EnvelopeGenerator.Service_legacy`.

Updated build configuration mappings to reflect the addition of `Service_legacy` and the removal of `Service`. Adjusted project nesting to place `Service_legacy` under the same parent as the removed `Service`.
2026-08-27 10:36:01 +02:00
36025fad7b Enhance handleReset functionality and fix async issues
Updated the `handleReset` method in `app.js` to await the
`Swal.fire` call, ensuring proper asynchronous handling.
Added logic to reset editable text form fields and restore
widget backgrounds after signing.

Refactored `app.min.js` to align with changes in `app.js`,
ensuring consistency between the minified and non-minified
versions. Improved code readability and maintainability
across both files.
2026-08-27 09:32:51 +02:00
0606efabf9 Update truncation limits for "position" and "city"
Increased the maximum width values for the "position" and "city" keys in the `TextMaxWidths` property of `Jobs.FinalizeDocument` (PDFBurnerParams.vb) from `1.25F` to `1.41F`. This change allows more text to fit before truncation occurs, improving text rendering flexibility.
2026-08-25 16:38:51 +02:00
2cb1e266f7 Improve annotation handling and UI stability
Updated `appsettings.UI.json` to adjust annotation properties for better alignment and positioning. Refactored `createAnnotations` and `createAnnotationFrameBlob` in `annotation.js` to improve readability, reusability, and asynchronous handling.

Enhanced the `App` class in `app.js` by introducing `signatureWidgets` for stable frame placement and added logic to find and update the nearest signature widget. Improved bounding box calculations and removed widget backgrounds after signing.

Refactored `handleAnnotationsCreate` to align with new logic and updated `app.min.js` to reflect these changes. General improvements include better code maintainability, reduced redundancy, and enhanced user experience.
2026-08-25 16:38:22 +02:00
35c1374b9c Update text truncation logic to use rendered width
Replaced character-based truncation with pixel-accurate truncation based on rendered width in inches. Updated `TruncateText` in `PDFBurner.vb` to use `Graphics.MeasureString` for width calculation and introduced a binary search for efficient truncation. Renamed `egName` to `fieldEgName` for clarity.

Replaced `TextMaxLengths` with `TextMaxWidths` in `PDFBurnerParams.vb`, specifying maximum render widths in inches. Updated default values for `position`, `city`, and `date`. Revised XML documentation to reflect the new truncation logic.
2026-08-25 09:47:22 +02:00
feaea1626b Fix signature field formatting and add text truncation
Address customer feedback on signature field layout issues:
- Added truncation logic in `PDFBurner.vb` to limit text length
  based on per-field maximums, appending a suffix (e.g., `..`).
- Enhanced `PDFBurnerParams.vb` with `TextMaxLengths` and
  `TruncationSuffix` for configurable truncation behavior.
- Improved spacing and layout handling for signature fields to
  prevent text collisions within fixed dimensions.
- Updated `EnvelopeGenerator.sln` with documentation for the
  signature formatting fix (`fix-signature-field-formatting.md`).
- Applied truncation consistently across `AddFormFieldValue`
  overloads and ensured deterministic rendering of annotations.
2026-08-25 09:07:02 +02:00
babd96200a Refactor and enhance PDF finalization process
- Added new imports to integrate additional services.
- Renamed `envelope` to `oEnvelopeEntity` for clarity.
- Improved envelope retrieval logic and error handling.
- Refactored `ReadOnly` envelope handling for readability.
- Reintroduced and enhanced annotation processing logic.
- Updated `BurnAnnotsToPDF` call with better logging.
- Added envelope domain model loading for report creation.
- Implemented signing certificate report generation.
- Merged burned document with signing certificate report.
- Updated output file naming and adjusted save logic.
- Improved method structure with clear steps and logging.
2026-08-24 14:33:03 +02:00
f9cfcf1f19 Fix: Add "Read and confirmed" label for RaC envelopes
Updated the Signature Certificate report to display the correct
label "Read and confirmed" for "Read and Sign" envelopes.

- Added `DocumentSignedRaC` resource key to `Model.resx` and
  `Model.en.resx` with localized values ("Gelesen und bestätigt"
  and "Read and confirmed").
- Updated `ReportItem.vb` to include conditional logic in
  `ItemStatusTranslated` for "Read and Sign" envelopes.
- Added strongly-typed property for `DocumentSignedRaC` in
  `Model.Designer.cs`.
- Updated `fix-report-label-read-and-confirmed.md` documentation
  to reflect changes, including testing steps and task completion.

These changes ensure accurate status labels in the report for
different envelope types.
2026-08-24 14:31:40 +02:00
ffb9e07466 Fix label for "Read and Sign" envelopes in reports
Addressed an issue where the Signature Certificate report incorrectly displayed "Document signed" for "Read and Sign" envelopes. Added a new resource key, `DocumentSignedRaC`, with translations ("Read and confirmed" / "Gelesen und bestätigt") to resolve this.

Updated `ItemStatusTranslated` in `ReportItem.vb` to use the new key for "Read and Sign" envelopes (`EnvelopeTypeId == 2`) while retaining existing behavior for other envelope types. Leveraged `Envelope.IsReadAndConfirm()` to determine envelope type.

Pending tasks include adding the new resource key to resource files and updating `Model.Designer.cs` with a strongly-typed property. Testing instructions provided to verify correct label behavior.
2026-08-24 13:47:28 +02:00
5384a08905 Update .gitignore to exclude specific files and directories
The .gitignore file was updated to ignore the following files and
directories:
- `FodyWeavers.xsd`
- `/EnvelopeGenerator.Tests.Application/annotations.json`
- `/EnvelopeGenerator.Server/EnvelopeGenerator.Server/TekH - SoftHSM Test.md`
- `/EnvelopeGenerator.Server/EnvelopeGenerator.Server/tekh_softHSM_test.md`
- `/EnvelopeGenerator.Server/EnvelopeGenerator.Server/publish-output`
- `/EnvelopeGenerator.Server/EnvelopeGenerator.Server/tekh_softHSM_test.md+/ignore/EnvelopeGenerator.Form`

These changes ensure that unnecessary or sensitive files are not
tracked in version control.
2026-08-20 14:58:51 +02:00
dda5ff28e1 Update build package paths to include PreRelease folder
Updated the `DesktopBuildPackageLocation` property in
`IISProfileNet8.pubxml` and `IISProfile.pubxml` to store
build packages in the `PreRelease` directory.

- Updated `EnvelopeGenerator.API.zip` and `EnvelopeGenerator.Server.zip` paths in `IISProfileNet8.pubxml`.
- Updated `EnvelopeGenerator.ReceiverUI.zip` path in `IISProfile.pubxml`.

No other properties or configurations were modified.
2026-07-06 14:32:18 +02:00
043349a47f Update EnvelopeGenerator.Server/EnvelopeGenerator.Server/README.md 2026-07-06 10:17:10 +02:00
c36f7ccf98 Merge branch 'feat/migr-DxReportViewer' into master 2026-07-02 15:59:31 +02:00
cf3e85bcd7 Update .gitignore and add new solution items
Updated the `.gitignore` file to exclude the `MigrationBackup/`
directory. Added `publish-output` and `tekh_softHSM_test.md`
to the `EnvelopeGenerator.Server` directory.

Modified the `EnvelopeGenerator.sln` file to include a new
solution item `RECEIVER_PDF_VIEWER_CONTEXT.md` under the
`src` project section. No structural changes were made to
existing projects in the solution.
2026-07-02 15:56:46 +02:00
77379265c2 Add dotnet-tools.json and publish script for .NET 8
Added a `dotnet-tools.json` file to define a .NET tool manifest, specifying the `dotnet-ef` tool with version `10.0.9`.

Introduced a `publish.bat` script for publishing the `EnvelopeGenerator.Server` project as a self-contained application targeting `win-x64` and .NET 8. The script handles cleaning, publishing, output verification, and provides deployment instructions for IIS.

Updated the `EnvelopeGenerator.sln` file to include a reference to the `publish.bat` script under the `EnvelopeGenerator.Server` project using a `SolutionItems` section.
2026-07-02 15:53:49 +02:00
9b68c09b0b Update project version to 1.0.1-beta
Updated `<Version>`, `<FileVersion>`, and `<AssemblyVersion>` in `EnvelopeGenerator.Server.csproj` to reflect a minor version update (1.0.0-beta -> 1.0.1-beta). This likely includes bug fixes, small improvements, or other non-breaking changes.
2026-07-02 15:51:36 +02:00
cd6bf05352 Add self-contained deployment and production config
Updated the IISProfileNet8.pubxml file to enable self-contained deployment by setting `<SelfContained>` to `true`. Target runtime was specified as `win-x64` using the `<RuntimeIdentifier>` property. Configured the environment for production by adding the `<EnvironmentName>` property set to `Production`.
2026-07-02 15:50:39 +02:00
b2c205b160 Add Publish & Deployment Guide to README.md
Added a detailed "Publish & Deployment Guide" to the README for the `EnvelopeGenerator.Server` project. The guide includes:

- Table of contents for easy navigation.
- Explanation of differences between `EnvelopeGenerator.Server` and a standard ASP.NET Core API.
- Justification for self-contained publishing and its benefits.
- Recommended `dotnet publish` command with parameter explanations.
- IIS configuration steps, including Application Pool settings, `web.config` adjustments, and module requirements.
- Verification checklist for published output files.
- Instructions for setting up logging and recycling the IIS Application Pool.
- Directory structure overview post-publish.
- Troubleshooting common deployment errors with solutions.

This guide ensures developers and administrators can successfully publish and deploy the application.
2026-07-02 15:44:36 +02:00
33439f4a65 Update .gitignore and modify server files
The `.gitignore` file was updated to ignore `FodyWeavers.xsd`.
Added a new directory `/publish-output` to the server folder.
The file `tekh_softHSM_test.md` was removed and re-added,
indicating potential modifications or reorganization.
2026-07-02 15:44:05 +02:00
b5cdf79799 Update .gitignore, add files, and modify configurations
Updated `.gitignore` to include `MigrationBackup/`. Added new files: `FodyWeavers.xsd`, `annotations.json`, and two markdown files for SoftHSM testing. Modified `/EnvelopeGenerator.Web/.config/dotnet-tools.json`, `/EnvelopeGenerator.GeneratorAPI/ClientApp/envelope-generator-ui/.vscode`, and `/EnvelopeGenerator.Tests.Application/Services/BugFixTests.cs` to reflect configuration and testing updates.
2026-07-02 12:44:12 +02:00
72915ac001 Add default descending sort to ID column in grid
Updated the `DxGridDataColumn` for the `Id` field in
`EnvelopeSenderPage.razor` to include default sorting.
The column now has `SortIndex=0` and `SortOrder=GridColumnSortOrder.Descending`,
ensuring the grid sorts by ID in descending order by default.
2026-07-02 02:13:56 +02:00
e4d7a0d8f3 Update project metadata and versioning scheme
Updated `<Version>` to `1.0.0-beta` to reflect a pre-release version. Aligned `<FileVersion>` and `<AssemblyVersion>` with the new versioning scheme (`1.0.0.0`). Replaced `<PackageOutputPath>` with `<Copyright>` for better metadata alignment. No changes to `<DocumentationFile>`.
2026-07-02 02:13:15 +02:00
d5f9de06f3 Update project metadata and enable XML documentation
Replaced `<TargetFramework>` with `<TargetFrameworks>` to allow for future multi-targeting. Enabled XML documentation file generation with `<GenerateDocumentationFile>`. Added project metadata including `<PackageId>`, `<Authors>`, `<Company>`, `<Product>`, and versioning properties (`<Version>`, `<FileVersion>`, `<AssemblyVersion>`). Included copyright notice and documentation file path configuration.
2026-07-02 02:05:47 +02:00
bbc129178f Add IIS publish profile for .NET 8.0 deployment
A new `IISProfileNet8.pubxml` file was added to configure publishing settings for a .NET 8.0 application. Key configurations include:

- Publish method set to `Package`.
- Build configuration set to `Release` and platform to `Any CPU`.
- Automatic site launch after publishing enabled.
- `App_Data` folder included in the package.
- Unique project identifier defined.
- Build package location specified with a version placeholder.
- Package created as a single file.
- IIS application path set to `EnvelopeGenerator`.
- Deployment target set to `IISWebDeployPackage`.
- Target framework set to .NET 8.0.
2026-07-02 02:03:37 +02:00
40c95100ff Add Title and Message fields to Envelope editor
Introduced input fields for "Title" and "Message" in the
EnvelopeSenderEditorPage, allowing users to specify metadata
for envelopes. The "Title" field is required and validated,
while the "Message" field is optional with a default value.

Updated the save logic to validate the title and display
appropriate error messages. Persisted the "Title" and
"Message" fields in the session cache via the
`EditorSessionData` record.

Modified the `CreateEnvelopeReceiverCommand` to use the
user-provided "Title" and "Message" values. Adjusted the UI
layout and styling to accommodate the new fields, ensuring
a seamless user experience.
2026-07-02 02:00:14 +02:00
1b2731b4b2 Simplify URL structure and update logging dependencies
Updated navigation logic and routes to remove the `/report` suffix, simplifying the URL structure for envelope-related pages.

- Updated `LoginReceiverPage.razor` and `ReceiverSignedPage.razor` to redirect to `/envelope/{EnvelopeKey}`.
- Changed `ReceiverPage.razor` route to `/envelope/{EnvelopeKey}`.
- Updated `ILogger` dependencies in `ReceiverPage.razor` and `ReceiverSignedPage.razor` to reflect new class names.
- Modified `EnvelopeReceiverPage.razor` route to `/envelope/{EnvelopeKey}/deprc`, indicating deprecation or restructuring.
2026-07-02 01:59:48 +02:00
ae56902758 Add SenderAuthCookieHandler for cookie-based JWT auth
Added a custom DelegatingHandler, SenderAuthCookieHandler, to forward the browser's Cookie header to outgoing HttpClient requests in Blazor Server. Registered the handler as a transient service and integrated it into the named HttpClient pipeline for internal API calls. This enables Blazor Server components to make authenticated API calls using cookie-based JWT authentication (AuthScheme.Sender).
2026-07-02 01:48:25 +02:00
e80059d34e Add envelope creation functionality
Introduced the ability to create envelopes with documents and receivers via a new `CreateAsync` method in `EnvelopeReceiverService`. Integrated this functionality into `EnvelopeSenderEditorPage.razor` with UI updates, including a save button spinner, validation checks, and a result popup for success or error feedback.

- Added `CreateAsync` method to handle `POST /api/EnvelopeReceiver` API calls.
- Injected `EnvelopeReceiverService` into `EnvelopeSenderEditorPage.razor`.
- Implemented save logic with validation for PDF upload, receivers, and signature fields.
- Added success and error popups for user feedback.
- Improved logging for envelope creation and validation warnings.
- Refactored save logic for better readability and maintainability.
2026-07-02 01:44:53 +02:00
a2443032c5 Add logging for stored procedure failure cases
Added warning logs in `EnvelopeReceiverController` to handle
cases where stored procedures return `OUT_SUCCESS=false`.
For `PRSIG_API_ADD_DOC_RECEIVER_ELEM`, log `DOC_ID`,
`RECEIVER_ID`, and `Page`. For `PRSIG_API_ADD_HISTORY_STATE`,
log `EnvelopeUuid`. These changes enhance error visibility
and debugging.
2026-07-02 01:12:44 +02:00
3d2fe532e5 Change @OUT_RECEIVER_ID type from int to bigint
Updated the SQL query in `EnvelopeReceiverAddReadSQL.cs` to change the data type of the `@OUT_RECEIVER_ID` variable from `int` to `bigint`. This modification ensures support for larger identifier values, addressing potential limitations of the previous `int` type.
2026-07-02 01:12:28 +02:00
5e83aa26c9 Refactor authentication and enhance logging
Updated `Authorize` attributes in multiple controllers to use
`AuthenticationSchemes = AuthScheme.Sender` instead of
`Policy = AuthPolicy.Sender`, reflecting a shift in the
authentication mechanism.

Added detailed logging in `EnvelopeReceiverController` to handle
cases where stored procedures return `OUT_SUCCESS=false`,
providing contextual information for debugging.

Removed unused SQL code and declarations in
`EnvelopeReceiverController` to improve code readability and
maintainability.
2026-07-02 01:11:09 +02:00
2c789cd4c0 Add dynamic color support for receivers
Introduced a `Color` property to `ReceiverDraft` and `SignatureFieldDraft` models, enabling dynamic color assignment from a predefined palette (`ReceiverPalette`). Updated the UI to reflect receiver-specific colors in the sender-receiver chips, placement mode hint bar, and signature placement button.

Refactored PDF rendering logic to dynamically derive visual styles (fill, border, and text colors) from receiver colors. Added a `HexToXColor` utility for converting hex color strings to `PdfSharp.Drawing.XColor`.

Removed hardcoded visual styles and replaced them with dynamic, receiver-specific styling. Simplified receiver addition logic to automatically assign colors from the palette. These changes improve clarity and maintainability while enhancing the user experience.
2026-07-01 23:27:38 +02:00
9ecfe08e2e Move session ID logic to OnAfterRenderAsync
The session ID generation and redirection logic was moved from
OnInitialized to OnAfterRenderAsync to address issues with
NavigationException during SSR prerendering. OnInitialized is
now intentionally left empty, and the new OnAfterRenderAsync
method ensures the session ID is appended to the URL only after
the first interactive render, when SignalR is connected and
NavigateTo is safe.
2026-07-01 23:10:39 +02:00
74da6e37b0 Refactor and enhance PDF signature editor
- Added `IMemoryCache` for session persistence and caching.
- Introduced session persistence via query parameter (`esid`).
- Replaced overlay-based click handling with normalized PDF coordinates.
- Added `PdfSharp` integration to render signature placeholders.
- Updated button behavior for receiver-specific signature placement.
- Improved receiver popup validation and email suggestion handling.
- Removed unused overlay synchronization logic.
- Refactored CSS for better PDF viewer layout and toolbar alignment.
- Enhanced logging with additional context for signature actions.
- General code cleanup for readability and maintainability.
2026-07-01 22:50:47 +02:00
10f65e583a Enhance IndexPage UI and navigation functionality
- Injected `NavigationManager` into `IndexPage.razor` to enable navigation.
- Improved layout alignment with additional classes in `<div>`.
- Added a new button for navigation to `/sender` with icons and text.
- Introduced a feature badge for "PDF-Export" with an SVG icon.
- Updated `.home-btn-primary` styles in `app.css`:
  - Ensured text color is always white with `!important`.
  - Made text bolder by increasing `font-weight` to `700`.
2026-07-01 20:09:44 +02:00
0cdc8f1191 Add "Cancel" button and navigation to sender page
Added the `NavigationManager` service injection to the
`EnvelopeSenderEditorPage.razor` file to enable navigation.
Introduced a "Cancel" button in the toolbar, styled with
`pdf-toolbar__btn pdf-toolbar__btn--reset`, which triggers
the `Cancel` method on click. The `Cancel` method navigates
the user to the `/sender` route, improving the user experience
by providing a clear way to cancel the current operation.
2026-07-01 19:28:30 +02:00
56ccab6377 Refactor email suggestion handling logic
Refactored the handling of email suggestions to separate selection
and commitment logic. Updated `<DxListBox>`'s `ValueChanged`
handler to use `OnReceiverEmailSuggestionCommittedAsync` for
finalizing selections. Introduced `SelectReceiverEmailSuggestion`
as a synchronous helper method for managing selection updates.
Centralized clearing of suggestions in the new
`OnReceiverEmailSuggestionCommittedAsync` method. Simplified
keyboard navigation logic by replacing asynchronous calls with
synchronous selection handling. These changes improve code
clarity and reduce unnecessary asynchronous operations.
2026-07-01 19:15:40 +02:00
4d069cdaa0 Merge branch 'feat/migr-DxReportViewer' of http://git.dd:3000/AppStd/EnvelopeGenerator into feat/migr-DxReportViewer 2026-07-01 17:16:19 +02:00
bbbfa4de01 Add phone number support for receivers in UI
Added functionality to display a receiver's phone number in the
sender-receiver chip if provided. Introduced a new input field
in the receiver popup for entering an optional phone number.

Updated the `ReceiverDraft` model and `_receivers` list to
include and store phone numbers. Modified methods to handle
phone number input and saving. Added CSS styles for displaying
the phone number in the sender-receiver chip.
2026-07-01 17:16:15 +02:00
3ff3373b27 Improve email suggestion handling and keyboard navigation
Enhanced the email suggestion feature by adding keyboard navigation support (`ArrowUp`, `ArrowDown`, `Enter`, `Escape`) and introducing `_selectedReceiverEmailSuggestion` to track the current selection. Updated methods to synchronize input and suggestions, pre-select matching suggestions, and reset the state when the popup is opened or closed. Improved error handling and ensured clean state management for better user experience.
2026-07-01 17:10:54 +02:00
2af8815cf6 Replace DxButton with HTML buttons and update styles
Replaced DevExpress DxButton components with standard HTML
<button> elements in EnvelopeSenderEditorPage.razor for
"Add Receiver" and "Add Signature" actions. The new buttons
include SVG icons and text for improved customization and
styling.

Updated envelope-viewer.css to add new styles for the
buttons, including `.sender-toolbar-action-btn` and
`.sender-toolbar-action-btn--compact` classes. Removed
unused styles related to DxButton components and adjusted
`.sender-receiver-chip__action` for proper width handling.

These changes improve design flexibility and maintainability.
2026-07-01 17:02:19 +02:00
ca24a96084 Add receiver management to EnvelopeSenderEditorPage
Introduced a new "Receivers" panel in `EnvelopeSenderEditorPage` to manage receivers. Added a popup for adding receivers with validation, email suggestions, and caching for performance. Updated the layout to display receiver details and signature fields.

Injected `EnvelopeReceiverPageDataService` for receiver-related operations. Added a `ReceiverDraft` model and implemented methods for managing receivers. Enhanced CSS for the new UI elements and ensured responsiveness. Minor refactoring and cleanup included.
2026-07-01 16:32:28 +02:00
db368b889a Merge branch 'feat/migr-DxReportViewer' of http://git.dd:3000/AppStd/EnvelopeGenerator into feat/migr-DxReportViewer 2026-07-01 15:43:33 +02:00
8f451b9c2c Implement navigation for CreateEnvelope method
Replaced the placeholder console log in the `CreateEnvelope` method with actual navigation functionality. The method now uses `Navigation.NavigateTo` to redirect users to the `/sender/editor` page, enabling the intended behavior for creating envelopes.
2026-07-01 15:43:26 +02:00
6120e6062e Update ReceiverController namespace and enhance Get method
The namespace of the `ReceiverController` class was updated from `EnvelopeGenerator.GeneratorAPI.Controllers` to `EnvelopeGenerator.Server.Controllers`.

The `Get` method was enhanced with the following changes:
- Added `[Authorize]` attribute with `AuthScheme.Sender`.
- Updated the method signature to include an optional `ReadReceiverQuery? receiver` parameter (defaulting to `null`) and a new `bool onlyEmailAddress` parameter (defaulting to `false`).
- Modified logic to handle `receiver` being `null` by creating a new `ReadReceiverQuery`.
- Added handling for `onlyEmailAddress` to return a list of email addresses if true.
- Simplified the result handling and removed the previous `receiver.HasAnyCriteria` logic.
2026-07-01 15:16:17 +02:00
cc2aea90ed Enhance query criteria and add partial email matching
- Added `[NotMapped]` attribute to `HasAnyCriteria` in `ReceiverQueryBase` to exclude it from database mapping.
- Made `HasAnyCriteria` in `ReceiverQueryBase` virtual for overriding.
- Introduced `EmailAddressSearch` in `ReadReceiverQuery` for partial email matching.
- Overrode `HasAnyCriteria` in `ReadReceiverQuery` to include `EmailAddressSearch`.
- Updated `ReadReceiverQueryHandler` to support partial email matching using `EF.Functions.Like`.
2026-07-01 15:15:50 +02:00
762a9e8bca Improve PDF viewer overlay synchronization
Refactor `EnvelopeSenderEditorPage.razor` to enhance the structure and behavior of the PDF editor wrapper:
- Add `class="pdf-editor-wrapper"` and update `overflow` to `auto`.
- Update `DxPdfViewer`'s `CssClass` to `sender-editor-pdf-viewer`.
- Introduce `OnAfterRenderAsync` to synchronize the overlay with the viewer.

Add new styles in `envelope-viewer.css` for better layout:
- Ensure `.pdf-editor-wrapper` and `.sender-editor-pdf-viewer` occupy full dimensions.
- Center and align content within the PDF viewer.

Enhance `envelope-editor.js` with `syncOverlayToPage`:
- Dynamically adjust overlay position and size relative to the viewer.
- Use `MutationObserver` and event listeners for real-time synchronization.
- Handle delayed rendering with scheduled sync attempts.

These changes improve overlay alignment, user experience, and code maintainability.
2026-07-01 14:26:11 +02:00
6ed4caea4f Merge branch 'feat/migr-DxReportViewer' of http://git.dd:3000/AppStd/EnvelopeGenerator into feat/migr-DxReportViewer 2026-07-01 13:59:29 +02:00
d94821433a Enable WebAssembly mode and add blazing-berry theme
Added the `@rendermode InteractiveWebAssembly` directive to
`IndexPage.razor` to enable interactive WebAssembly rendering.
Included a `<link>` element to reference the `blazing-berry.bs5.min.css`
stylesheet from the `DevExpress.Blazor.Themes` content folder to
apply the "blazing-berry" theme for enhanced styling.
2026-07-01 12:58:25 +02:00
278b9964f1 Update signing confirmation text and logout navigation
The user-facing text in `EnvelopeReceiverReportSignedPage.razor` has been updated to provide a more detailed and formal confirmation message for signing a document.

- Replaced "Dokument erfolgreich unterschrieben" with "Möchten Sie das Dokument verbindlich unterschreiben?".
- Updated the follow-up message to clarify the irreversibility of the action and the electronic signing process.

Additionally, the logout navigation behavior has been modified:
- Changed the post-logout redirect from `/envelope/login/{EnvelopeKey}` to the root page (`/`), while retaining the `forceLoad` parameter.
2026-07-01 12:56:51 +02:00
e6722803bb Add submit confirmation popup and logout functionality
Added dependency injection for `AuthService`, `ReceiverAuthorizationService`, `PageDataService`, and `Logger` to enable their usage in the component. Introduced a "Submit" button in the UI to confirm the signing process and complete the workflow.

Implemented a `DxPopup` component to display a confirmation dialog when the "Submit" button is clicked. The popup includes a message about the successful signing of the document and asks the user to confirm whether to complete the process and log out.

Added state variables `_isLoggingOut` and `_submitConfirmVisible` to manage the popup visibility and logout process. Created `OpenSubmitConfirmPopup` to toggle the popup and `SubmitAndLogoutAsync` to handle the submission process, including logging out via `AuthService` and navigating to the login page.

Updated the `@code` block with the new state variables and methods for managing the submit and logout functionality.
2026-07-01 12:36:50 +02:00
47bc7675c9 Handle cache miss and redirect in SignedPage
Added a check for `_sig` being `null` to handle cache misses or missing `sid`. Logged a warning with `Sid` and `EnvelopeKey` details when this occurs. Implemented a redirection to the report page (`/envelope/{EnvelopeKey}/report`) using `Navigation.NavigateTo` with `forceLoad: true`. Added an early return to prevent further execution after redirection.
2026-07-01 11:47:09 +02:00
789e312316 Refactor signature box layout and improve readability
Refactored the signature box layout to dynamically calculate
positions based on content, improving flexibility and precision.
Introduced new constants (`lineH`, `bgPad`) to standardize
spacing and replaced hardcoded values for better maintainability.
Adjusted background rectangle sizing to fit content dynamically
and improved text layout logic to handle optional fields more
gracefully. Simplified image area logic and reduced redundant
calculations. Overall, improved code readability and alignment
for a cleaner, more compact layout.
2026-07-01 11:38:07 +02:00
2a9bbb3fe5 Add envelope editor with PDF upload and signature tools
Introduced a new Blazor page `EnvelopeSenderEditorPage.razor` for editing envelopes with an interactive interface. Integrated `DxPdfViewer` for rendering PDFs and added functionality for uploading, viewing, and interacting with PDF files.

Key features:
- Action bar with buttons for uploading PDFs, toggling signature placement mode, clearing fields, and saving.
- Placement mode for adding signature fields via an overlay, with visual placeholders.
- JavaScript interop (`envelope-editor.js`) for precise click coordinate mapping.
- Error handling for unsupported file types and size limits (50 MB).
- Logging for debugging key actions like PDF uploads and field placements.

Defined constants for accurate signature field dimensions and scaling. Added models (`SignatureFieldDraft`, `OverlayCoords`) to manage state and interactions.
2026-07-01 11:25:04 +02:00
bc34317720 Adjust signature box layout and add background color
Updated the signature box proportions by adjusting `imgRatio` to 52% and `textRatio` to 43%. Added a cream-tone background with extra padding (`bgPad`) and rendered it behind the signature content. Tightened gaps between the image, separator line, and text area. Increased text row height slightly for better spacing.
2026-07-01 11:12:16 +02:00
76ff3e47e1 Render signatures on PDF using PdfSharp
Added functionality to render captured signatures onto a PDF document using the PdfSharp library. Introduced a new `_signatures` field to store signature data and updated `OnInitializedAsync` to fetch and process signatures. Implemented the `DrawSignaturesOnPdf` method to overlay signature images, separator lines, and signer details (name, position, place, date) onto the PDF. Added a helper method `DataUrlToBytes` for decoding Base64-encoded signature images. Defined constants for layout and styling to ensure consistent rendering. Updated `Dispose` to clean up resources.
2026-07-01 10:30:35 +02:00
2d22bfcd06 Simplify signed document viewer logic
Removed all signature capture and validation functionality, including the signature popup, JavaScript interop, and related backend logic. Simplified the `DxReportViewer` initialization to directly display the signed document. Added support for retrieving cached signatures via the `sid` query parameter. Streamlined error handling, logging, and page metadata. Cleaned up unused imports, constants, and methods to reduce complexity.
2026-07-01 09:57:19 +02:00
185c783824 Improve signature handling and navigation stability
Updated button logic to display "Unterschreiben" only when signature fields exist. Changed `_signaturePopupVisible` to always be `false` for consistent popup behavior. Improved navigation after caching a signature by nullifying `_report`, adding a delay for UI updates, and using `forceLoad: true` for clean circuit teardown. These changes enhance user experience and prevent potential crashes.
2026-07-01 09:55:55 +02:00
b957b4b4bb Update navigation path for successful login
Changed the navigation path in `LoginReceiverPage.razor` to redirect users to `/envelope/{EnvelopeKey}/report` instead of `/envelope/{EnvelopeKey}` upon a successful login. The `forceLoad: true` parameter remains unchanged to ensure a full page reload.
2026-07-01 09:54:59 +02:00
df154d83cc Refactor signature caching and navigation logic
Replaced the previous signature persistence mechanism with
`IMemoryCache` for temporary storage of captured signatures
using a unique `Guid` key and a 1-minute TTL. Added logging
to track cached signatures and their associated envelope keys.

Removed the logic for rebuilding and displaying reports with
overlaid signatures. Instead, implemented navigation to a
new signed page (`/envelope/{EnvelopeKey}/signed`) with the
signature ID passed as a query parameter.
2026-07-01 01:41:19 +02:00
49ec9fbead Add PdfSharp font resolver for .NET 8 compatibility
Added a `PdfSharpFontResolver` class to enable font resolution
for PdfSharp in .NET 8, addressing the lack of system font
access. The resolver reads fonts from the Windows Fonts folder
and supports the Arial font family. Registered the resolver
globally in `Program.cs` using `GlobalFontSettings.FontResolver`.

Updated `Program.cs` with comments explaining the necessity of
the resolver for .NET 8. The resolver includes methods to map
font family names to specific font files and load font data.
Throws a `FileNotFoundException` if required fonts are missing.

Made minor formatting changes in `Program.cs` without altering
the `SwaggerDoc` description functionality.
2026-07-01 01:25:24 +02:00
01fc29f59e Refactor signature handling and add signed page UI
Refactored `EnvelopeReceiverReportPage.razor` to replace the logic for burning captured signatures with a new approach that draws placeholder boxes for signature fields using the `DrawSignaturePlaceholders` method. Removed the `BurnSignaturesIntoPdf` method and introduced `PDFsharp` for rendering placeholders.

Added `EnvelopeReceiverReportSignedPage.razor` to handle signed envelopes, including a detailed UI for document display, metadata, and a signature popup with "Draw," "Text," and "Image" modes. Integrated JavaScript interop for signature creation and validation.

Updated `EnvelopeGenerator.Server.csproj` to include the `PDFsharp` library. Enhanced error handling, logging, and UI feedback. Improved code readability and maintainability through cleanup and refactoring.
2026-07-01 01:25:09 +02:00
733b70cca2 Refactor PDF handling; remove iText dependency
Replaced iText-based PDF processing with DevExpress PdfGraphics API.
Removed `itext` and `itext.bouncy-castle-adapter` dependencies.
Simplified `BuildReport` to burn signatures directly into PDFs
and render all pages using `XRPdfContent` with `GenerateOwnPages = true`.
Consolidated subreport logic into `BuildReport` and removed
`BuildPageSubreport`. Eliminated unused constants and methods,
including `GetPdfPageCount`. Updated XML documentation to reflect
the new implementation. Placeholder implementation for
`BurnSignaturesIntoPdf` added, pending further development.
2026-06-30 23:55:39 +02:00
8f4b751303 Add envelope report page with signature capture
Added a new Razor page `EnvelopeReceiverReportPage.razor` to display and manage envelope reports at the route `/envelope/{EnvelopeKey}/report`. Integrated DevExpress Blazor Reporting components (`DxReportViewer`, `DxPopup`) for rendering PDF documents and capturing user signatures.

Implemented a multi-tab signature capture interface supporting drawing, text input with font selection, and image uploads. Added support for dynamically overlaying captured signatures on PDF documents using `XRPictureBox`.

Introduced dependency injection for services like `AuthService`, `ReceiverAuthorizationService`, and `PageDataService` to handle authentication, data retrieval, and logging. Included lifecycle methods for user authorization, PDF loading, and restoring cached signatures.

Added validation for signature input, error handling for missing data, and utility methods for building reports, extracting PDF page counts, and converting base64 data URLs. Integrated JavaScript interop for canvas-based signature handling.

Included custom styles and assets, and implemented disposal logic for cleaning up resources.
2026-06-30 23:27:20 +02:00
a5e4f97397 Migrate PDF.js to DxPdfViewer in receiver signing page
This commit introduces a detailed migration plan to replace the
`PDF.js` rendering engine with `DxPdfViewer` in the receiver
signing experience (`EnvelopeReceiverPage.razor`). The migration
preserves the existing signing workflow and behavior while
introducing a new rendering layer.

Key changes:
- Replaced `PDF.js` rendering surface with `DxPdfViewer`.
- Preserved page-level orchestration for authorization, document
  loading, signature handling, and toolbar interactions.
- Introduced a custom overlay adapter for signature placeholders
  and applied signature overlays.
- Centralized page/zoom geometry acquisition for overlay alignment.
- Maintained signature navigation logic and thumbnail sidebar
  behavior.
- Updated `pdf-viewer.js` to separate engine-specific logic and
  adapt it for `DxPdfViewer`.
- Updated styles in `envelope-viewer.css` to support the new viewer.

This migration ensures that all existing workflow behaviors remain
functional, including navigation, zoom, signature placement, and
validation, while transitioning to the new rendering engine.
2026-06-29 11:04:53 +02:00
6ca03a50eb Add documentation for receiver PDF viewer context
Added `RECEIVER_PDF_VIEWER_CONTEXT.md` to the `src` project, documenting the current implementation and behavior of the receiver-side PDF viewing and signing experience in the `EnvelopeGenerator` project.

The document outlines the use of `PDF.js` as the current rendering engine, the planned migration to `DxPdfViewer`, and the functional capabilities that must be preserved. Key features include single-page PDF viewing, navigation, zoom, signature overlays, and metadata validation.

This addition ensures clarity for future development and emphasizes the importance of maintaining existing workflows during the migration or other changes.
2026-06-29 10:56:10 +02:00
96a84ba1a5 Update documentation to reflect current architecture
Revised COPILOT_CONTEXT.md to align with the active
EnvelopeGenerator architecture and workflows. Key updates:
- Updated title and purpose for clarity.
- Replaced migration notice with active app structure details.
- Documented hosting model, including `Program.cs` setup.
- Removed outdated deployment architecture section.
- Reorganized route structure for WebAssembly and server pages.
- Expanded authentication model for sender/receiver flows.
- Added details on server-side data loading and caching.
- Updated receiver PDF viewer and signature workflow sections.
- Clarified coordinate system conversions and usage.
- Marked deprecated projects and legacy files as "Do Not Touch."
- Replaced mistakes history with workspace rules.
- Updated last modified date to 2026-06-29.
2026-06-29 10:23:24 +02:00
ec0ea72890 Migrate authentication to SSR service for EnvelopeReceiver
Migrated the `EnvelopeReceiverPage.razor` component from using a WASM client-side authentication service to a server-side rendering (SSR) authentication service. This resolves issues caused by self-referencing HTTP requests in SSR contexts.

- Added `IEnvelopeAuthService` interface and `EnvelopeAuthService` implementation to validate user authentication and envelope key claims directly via `HttpContext.User`.
- Registered `EnvelopeAuthService` in DI container with a scoped lifetime.
- Updated `EnvelopeReceiverPage.razor` to use `IEnvelopeAuthService` for authentication checks and `IHttpClientFactory` for logout functionality (changes reverted due to merge conflict).
- Improved authentication flow by eliminating HTTP overhead and ensuring compatibility with SSR.
- Remaining tasks include re-applying page changes, testing, and updating documentation.

This migration ensures a cleaner, more reliable authentication mechanism for SSR pages.
2026-06-29 10:21:47 +02:00
7b912387e7 Refactor EnvelopeReceiverPage to server-side logic
Updated the document signing system to use a unified Blazor Auto (Server+WASM hybrid) frontend. Replaced client-side API calls with server-side authentication and data loading via `EnvelopeReceiverAuthorizationService` and `EnvelopeReceiverPageDataService`.

- Updated `/envelope/{key}` route to use MediatR for data loading.
- Integrated PDF.js 3.11.174 for rendering with configurable quality.
- Removed iText7 dependency due to GPL license issues.
- Introduced per-envelope cookies for receiver authentication.
- Cached signatures now loaded from distributed cache.
- Replaced redundant client-side API calls with server-side logic.
- Improved security and performance with server-side authorization.

These changes streamline the workflow, enhance security, and align the system with modern Blazor Server practices.
2026-06-29 09:57:50 +02:00
6c142eba08 Refactor signature processing in EnvelopeReceiverPageDataService
Refactored the logic to filter and map `elements` to `signatures`
before converting them to `UnitOfLength.Point`. Removed the direct
return of `elements` and ensured that only the processed `signatures`
are converted and returned. Added a `ToList()` call to materialize
the `signatures` collection before conversion.
2026-06-29 01:29:33 +02:00
489d2808a1 Refactor EnvelopeReceiverPage for modular data handling
Refactored `EnvelopeReceiverPage.razor` to use new services for receiver authentication and data retrieval. Introduced `EnvelopeReceiverAuthorizationService` for handling JWT-based authorization and `EnvelopeReceiverPageDataService` for centralized data access and caching. Updated dependency injection in `Program.cs` to register these services.

Replaced direct service calls with `PageDataService` methods for document, signature, and receiver data retrieval. Improved logging with `ILogger` and added debug logs for token validation. Enhanced modularity, maintainability, and performance by consolidating logic and reducing coupling between components.
2026-06-29 01:26:43 +02:00
7466fd78f6 Update auth, JSON config, and sender dashboard styles
Modified the `[Authorize]` attribute in `EnvelopeController` to use `AuthScheme.Sender` for authentication. Updated `Program.cs` to configure JSON serialization with `ReferenceHandler.IgnoreCycles` to handle circular references.

Added new CSS styles for the sender dashboard in `sender-page.css`, including layout, action bars, buttons, tabs, badges, and responsive design improvements. Enhanced button states and introduced styles for status indicators and receiver badges.
2026-06-28 22:24:33 +02:00
e34b5ddbbe Update render mode in EnvelopeSenderPage.razor
Replaced the default `InteractiveWebAssembly` render mode with a custom `InteractiveWebAssemblyRenderMode` instance, explicitly setting `prerender` to `false`. This change disables prerendering to adjust the page's rendering behavior, potentially optimizing performance or meeting specific rendering requirements.
2026-06-28 22:05:16 +02:00
b56f906848 Refine authorization and rendering mechanisms
Updated `EnvelopeSenderPage.razor` to replace the `[Authorize]`
attribute with the `@rendermode InteractiveWebAssembly` directive,
indicating a shift in how authorization or rendering is handled.

Modified the `Check` method in `AuthController.cs` to specify
`AuthenticationSchemes = AuthScheme.Sender` in the `[Authorize]`
attribute, enforcing a more specific authentication scheme for
this endpoint.
2026-06-28 21:39:33 +02:00
fe09c5c7ae Update auth-hub primary destination address in yarp.json
Replaced the `Address` value for the `primary` destination in the
`auth-hub` cluster within `yarp.json`. The previous value
(`https://localhost:9090`) was updated to
`http://172.24.12.39:9090`, reflecting a move from a local
development environment to a specific networked environment.
The protocol was also changed from `https` to `http`.
2026-06-28 20:31:52 +02:00
0763d82f6e Refactor services to use IHttpClientFactory
Refactored `DocReceiverElementService` and `EnvelopeService` to use `IHttpClientFactory` instead of directly injecting `HttpClient`, improving flexibility and testability.

Updated constructors to accept `IHttpClientFactory` and replaced direct `HttpClient` usage with named clients (`"EnvelopeGenerator.Server"`). Adjusted methods to use the factory-created clients for HTTP requests.

Added `using Microsoft.Extensions.Options;` in `Program.cs` and registered `EnvelopeService` and `DocReceiverElementService` in the dependency injection container for proper resolution. Clarified their usage in SSR scenarios with comments.

Removed redundant `using` directives and aligned imports with the updated implementation.

These changes enhance maintainability, scalability, and testability by leveraging `IHttpClientFactory` for better HTTP client management and dependency injection.
2026-06-28 20:31:30 +02:00
5a30bc050b Refactor services to remove ApiOptions dependency
Simplified `DocReceiverElementService` and `EnvelopeService` by removing the `ApiOptions` dependency. Updated constructors to eliminate the `IOptions<ApiOptions>` parameter and switched to using relative URLs directly for API requests.

Removed unused `BaseUrl` and `UsePredefinedReports` properties from `ApiOptions`. Cleaned up redundant fields, constructor logic, and unused `using` directives in affected services.

Registered `EnvelopeService` in `Program.cs` with `AddScoped`. These changes improve maintainability, reduce configuration overhead, and make the services more self-contained.
2026-06-28 20:16:34 +02:00
a4b218b9f3 Add new namespaces to EnvelopeService.cs
Included `EnvelopeGenerator.Application.Common.Dto`, `EnvelopeGenerator.Server.Client.Models`, `EnvelopeGenerator.Server.Client.Options`, and `Microsoft.AspNetCore.WebUtilities` to support new functionality or dependencies in the `EnvelopeService.cs` file.
2026-06-25 15:35:36 +02:00
67798b35da Simplify GetDocument authorization logic
Refactor `DocumentController.GetDocument` to exclusively support the "Sender" role by removing logic for the "Receiver" role. Update the `[Authorize]` attribute to enforce the `AuthPolicy.Sender` policy instead of `AuthPolicy.SenderOrReceiver`.

Remove the `AuthPolicy.SenderOrReceiver` policy from `Program.cs` authorization configuration, reflecting the decision to separate role-based access more explicitly. The application now defines distinct policies for "Sender" and "Receiver" roles without combining them.
2026-06-25 15:18:20 +02:00
b5bb2bbaae Refactor sender page and auth service logic
- Added project reference to `EnvelopeGenerator.Application` in the client project.
- Updated imports and injected services in `EnvelopeSenderPage.razor`.
- Improved null handling for `EnvelopeReceivers` and updated email display logic.
- Replaced `CheckSenderAsync` with `CheckSenderAccessAsync` for authorization.
- Refactored `GetStatusInfo` to use `EnvelopeStatus` enum directly.
- Added `CheckSenderAccessAsync` and `LogoutSenderAsync` methods in `AuthService`.
- Simplified `Logout` logic in `AuthController` to remove redundant checks.
2026-06-25 15:17:57 +02:00
85a0736106 Add envelope history tracking and receiver signing status
Added a `Histories` property to `EnvelopeDto` to track envelope
history entries for actions like `DocumentSigned` and
`EnvelopeOpened`. Introduced a computed `Signed` property in
`EnvelopeReceiverDto` to determine if a receiver has signed the
envelope based on the history. Updated `using` directives to
support these changes.
2026-06-25 14:43:09 +02:00
de9c9da176 Add Microsoft.AspNetCore.WebUtilities package
Added a new package reference for `Microsoft.AspNetCore.WebUtilities` (version 8.0.28) to the `EnvelopeGenerator.Server.Client.csproj` file. This package provides utilities for handling web-related functionality, such as query string parsing and encoding, and may support new features or dependencies in the project.
2026-06-25 13:37:21 +02:00
f4571320ce Mark Auth record as obsolete with replacement guidance
The `Auth` record in the `EnvelopeGenerator.Server.Models`
namespace has been marked as `[Obsolete]` with the message
"Use auth DTO" to indicate it is outdated and should be
replaced with a newer implementation.

The `Auth` record includes the following properties:
- `AccessCode`, `SmsCode`, `AuthenticatorCode` (nullable strings)
- `UserSelectSMS` (boolean)

Additionally, it defines computed properties `HasAccessCode`
and `HasSmsCode` to check for the presence of `AccessCode`
and `SmsCode`, respectively.
2026-06-25 13:37:05 +02:00
2abfffdeba Remove @rendermode and fix German special characters
The `@rendermode InteractiveWebAssembly` directive was removed from `IndexPage.razor` to simplify the page setup.

Additionally, the `HomePageDescription` constant was updated to replace improperly encoded characters with their correct Unicode equivalents. This ensures proper rendering of German umlauts and special characters in the text.
2026-06-25 13:22:38 +02:00
bfd1a9d060 Enhance EnvelopeSenderPage with new layout and features
Redesigned `EnvelopeSenderPage.razor` to include a structured
dashboard layout with a sender action bar and dynamic content
area. Added authorization enforcement with the `@attribute`
directive and dependency injection for services like
`EnvelopeService` and `AuthService`.

Introduced a tabbed interface for managing "Active" and
"Completed" envelopes, with a `DxGrid` component for displaying
envelope details. Added support for filtering, searching, and
row selection, along with detailed row templates for receiver
information.

Implemented methods for loading, refreshing, creating, editing,
and deleting envelopes, as well as logging out. Enhanced error
handling and state management, and added console logging for
debugging. Integrated external stylesheets for improved UI.
2026-06-25 13:22:09 +02:00
78ed49a077 Refactor AuthService and add new service classes
Refactored `AuthService` to introduce a reusable `CreateDefaultClient` method, reducing code duplication. Updated all relevant methods in `AuthService` to use this new method.

Added `CultureService` to manage application culture/localization, including support for setting, getting, and initializing culture from `localStorage` or browser settings.

Introduced `DocReceiverElementService` for retrieving document receiver elements (signatures) and `EnvelopeService` for managing envelope data retrieval with optional filters. Both services include error handling and consistent JSON deserialization.

These changes improve code maintainability, reusability, and adhere to the single responsibility principle.
2026-06-25 13:16:57 +02:00
6aa97adf84 Refactor and enhance EnvelopeReceiverPage UI/UX
- Replaced `SignatureService` with `DocReceiverElementService` in DI.
- Refactored `envelope-action-bar` for better readability and added badges for `2FA`, `Access Code`, and `Signature Count`.
- Improved error handling and loading states with clearer messages.
- Enhanced PDF viewer toolbar with better navigation, zoom, and signature controls.
- Added resizable thumbnail sidebar with persistent width settings.
- Refactored signature popup to support draw, text, and image tabs with validation.
- Improved JavaScript interop for PDF rendering and signature handling.
- Introduced DevExpress PDF Viewer as an alternative implementation.
- Consolidated state management and improved code readability.
2026-06-25 13:10:32 +02:00
7456babe0d Merge branch 'master' into bugfix/devexpress-pdf-not-displaying 2026-06-24 16:17:40 +02:00
71e375d6ea Introduce SSR authentication service for EnvelopeReceiverPage
Replaced the WASM client-side authentication service with a new
SSR (Server-Side Rendering) authentication service to resolve
issues in SSR mode caused by self-referencing HTTP requests and
the lack of `HttpContext`.

Added `IEnvelopeAuthService` interface and its implementation
`EnvelopeAuthService`, which directly accesses `HttpContext.User`
to validate user authentication and claims. Registered the service
in the DI container with a scoped lifetime.

Updated `EnvelopeReceiverPage.razor` to use the new SSR service
for authentication checks and logout logic. Changes to the page
were reverted due to a merge conflict, with a detailed plan
provided for re-application.

Improved logging for debugging authentication issues and outlined
a migration checklist, including testing, unit tests, and
documentation updates. These changes improve performance, ensure
SSR compatibility, and eliminate unnecessary HTTP requests.
2026-06-24 16:08:29 +02:00
05f64e2b61 Refactor LoginReceiverPage for readability and visuals
Updated SVG paths for improved icon rendering and appearance.
Reformatted code for consistent style, including moving braces
to new lines and improving indentation. Enhanced error handling
logic for `LoginResult` states with better readability. Updated
password input field toggle logic and replaced SVG icons for
show/hide functionality. Refactored `SubmitAsync` and
`OnKeyDownAsync` methods for clarity. Cleaned up unnecessary
whitespace and ensured overall maintainability.
2026-06-24 15:59:21 +02:00
ed17852542 Add EnvelopeAuthService for SSR authentication
Introduced `EnvelopeAuthService` and `IEnvelopeAuthService` to handle server-side authentication for envelope receiver pages.

- Registered `IEnvelopeAuthService` as a scoped service in `Program.cs`.
- Implemented `EnvelopeAuthService` to validate user authentication and envelope key matching using `IHttpContextAccessor` and JWT claims.
- Added methods to retrieve the authenticated envelope key and current user (`ClaimsPrincipal`).
- Prioritized `NameIdentifier` claim for envelope key extraction, with fallback to `sub` claim.
- Documented the service and interface with XML comments for clarity.

This centralizes authentication logic, ensuring reusability and adherence to SSR best practices.
2026-06-24 15:57:06 +02:00
9947774ba8 Add YARP reverse proxy for auth request forwarding
Added the `Yarp.ReverseProxy` package and configured the app to use
YARP for forwarding specific authentication-related API requests
to an external service (`auth-hub`). Updated `Program.cs` to load
YARP configuration from a new `yarp.json` file and added middleware
to map unmatched requests to the reverse proxy.

Replaced old routes and clusters with new routes (`auth-login`,
`auth-envelope-receiver-login`) and a new cluster (`auth-hub`)
pointing to `https://localhost:9090`. Configured route
transformations for path and query parameter adjustments.

These changes improve modularity and scalability by enabling
dynamic reverse proxy configuration and external service
integration.
2026-06-24 15:55:56 +02:00
c6c1decd2a Refactor to use IHttpClientFactory and remove ApiOptions
Replaced direct injection of HttpClient with IHttpClientFactory
across the codebase to improve HTTP client management and align
with best practices. Removed dependency on ApiOptions and
IOptions<ApiOptions> in multiple services, simplifying constructors
and reducing configuration complexity.

Updated FontLoader to use IHttpClientFactory for font loading
with relative paths. Adjusted comments and documentation to
reflect these changes. Cleaned up unused using directives
related to ApiOptions.
2026-06-24 10:01:19 +02:00
0fdaa1a38d Refactor HttpClient usage and simplify configuration
Updated HttpClient setup to use named clients for API calls
and DevExpress components, improving resource management
and scalability. Scoped a default HttpClient for PdfViewer
requirements. Removed ApiOptions configuration binding for
simplification. Updated FontLoader to use IHttpClientFactory
to align with modern best practices.
2026-06-24 10:01:03 +02:00
5d66de9f32 Refactor HttpClient and HttpContextAccessor setup
Moved HttpContextAccessor registration into the configuration
of the named HttpClient ("EnvelopeGenerator.Server") to support
server-side rendering (SSR) scenarios. Updated the HttpClient
to dynamically set its BaseAddress based on the current request's
scheme and host using HttpContextAccessor. Removed standalone
HttpContextAccessor registration and updated related comments.
2026-06-24 10:00:43 +02:00
b6ec5307b6 Refactor HTTP client management and service lifetimes
Updated DependencyInjection.cs to change ISmsSender and
IEnvelopeSmsHandler lifetimes from Singleton to Scoped,
ensuring per-request instantiation. Added Microsoft.Extensions.Http
package to EnvelopeGenerator.Server.Client.csproj for enhanced
HttpClient handling. Refactored AnnotationService, AuthService,
DocumentService, EnvelopeReceiverService, SignatureCacheService,
and SignatureService to use IHttpClientFactory, improving
flexibility and testability. Introduced a named HttpClient
"EnvelopeGenerator.Server" in Program.cs for internal API calls,
and removed the previous HttpClient setup using HttpContextAccessor.
Added necessary using directives for System.Net.Http across
service files to support these changes.
2026-06-22 17:35:00 +02:00
106e62a912 Refactor namespaces to EnvelopeGenerator.Server
Renamed namespaces and related identifiers from EnvelopeGenerator.WebUI
to EnvelopeGenerator.Server across the project. This change affects
data models, services, controllers, and configuration files to ensure
consistency with the new architecture.

Updated @using directives in Razor components and other files to
reflect the new namespace structure. Adjusted project references in
EnvelopeGenerator.Server.csproj to point to the new
EnvelopeGenerator.Server.Client project.

Modified middleware and logging configurations to use the new
EnvelopeGenerator.Server namespace, including changes in Program.cs
and appsettings.json.

Updated resource and file references to use the new
EnvelopeGenerator.Server path, ensuring correct resource loading.

Adjusted configuration options in Program.cs to use the new namespace
for options classes, such as ApiOptions and PdfViewerOptions.

Updated authentication scheme names and related constants to align
with the new namespace structure.

Revised comments and documentation to reflect the new namespace,
ensuring clarity and consistency in the codebase.
2026-06-22 16:14:11 +02:00
27940f5d34 Refactor project structure in solution
Replaced "EnvelopeGenerator.WebUI" with "EnvelopeGenerator.Server" and "EnvelopeGenerator.WebUI.Client" with "EnvelopeGenerator.Server.Client". Updated project entries, solution configuration platforms, and nested projects to reflect these changes.
2026-06-22 15:17:34 +02:00
e776c2edb4 Update launchSettings.json with new profiles and URLs
Updated the `$schema` URL to use HTTPS. Modified `iisSettings` with new `applicationUrl` and `sslPort`. Removed old profiles (`http`, `https`, `IIS Express`) and added new ones: `https (Blazor UI)`, `https (Swagger API)`, `http (Development)`, and updated `IIS Express`. Removed `inspectUri` from `IIS Express` profile.
2026-06-22 15:06:46 +02:00
3f0f5d7fb9 Add Jenkins pipeline and update JSON comments
A new Jenkins pipeline has been added to the `Jenkinsfile` with a 'Build' stage executing `dotnet build`. The `appsettings.Development.json` file has been reformatted for consistency. In `appsettings.json`, comments have been added to explain the "Content-Security-Policy" nonce usage, logging levels, and the naming format for resource files in the `Cultures` section, aiding in localization management.
2026-06-22 14:57:53 +02:00
e11bc9df8e Add new controllers for envelope management
Introduced multiple controllers to enhance application functionality:
- `AnnotationController`: Manages annotations and signature lifecycle.
- `AuthController`: Handles user authentication and session management.
- `CacheController`: Manages cached data for receivers.
- `ConfigController`: Exposes client configuration data.
- `DocumentController`: Provides access to envelope documents.
- `EmailTemplateController`: Manages email templates.
- `EnvelopeController`: Manages envelope operations.
- `EnvelopeReceiverController`: Handles envelope receiver data.
- `EnvelopeTypeController`: Retrieves envelope types.
- `HistoryController`: Accesses envelope history.
- `IAuthController`: Defines authentication interface.
- `LocalizationController`: Manages localization settings.
- `ReadOnlyController`: Manages read-only envelope sharing.
- `ReceiverController`: Retrieves receiver data.
- `SignatureController`: Retrieves document signatures.
- `TfaRegistrationController`: Manages two-factor authentication.

These changes improve maintainability and scalability by organizing operations into dedicated controllers.
2026-06-22 14:57:26 +02:00
4dca17d39c Add claim extension methods for user authentication
Introduce `ReceiverClaimExtensions` and `SenderClaimExtensions` classes in the `EnvelopeGenerator.API.Extensions` namespace. These classes provide methods to extract specific claims from a `ClaimsPrincipal` object, aiding in user authentication.

In `ReceiverClaimExtensions.cs`, add methods to retrieve envelope-specific claims such as `EnvelopeUuid`, `ReceiverSignature`, `ReceiverMail`, `EnvelopeId`, and `ReceiverId`. Implement `GetRequiredClaimValue` to handle missing claims.

In `SenderClaimExtensions.cs`, add methods to extract sender-related claims like `GetId`, `GetUsername`, `GetName`, `GetPrename`, and `GetEmail`. Implement `GetRequiredClaimOfSender` for handling missing claims.

Both classes include XML documentation for clarity on method usage and exceptions.
2026-06-22 14:56:57 +02:00
8baf6b5553 Add AuthProxyDocumentFilter for Swagger customization
Introduce AuthProxyDocumentFilter to enhance OpenAPI docs by
adding custom operations for login and envelope receiver
login. Implement methods to define POST operations at
`/api/auth` and `/api/Auth/envelope-receiver/{key}` paths,
including request parameters and response descriptions.
Include necessary using directives for OpenAPI support.
2026-06-22 14:56:21 +02:00
3ca99fdd83 Add models for auth, contact, culture, and annotations
Introduce new classes and records in the `EnvelopeGenerator.API.Models` namespace to handle various functionalities:

- Add `Auth` record for managing authentication codes.
- Introduce `ContactLink` class for hyperlink management.
- Add `Culture` and `Cultures` classes for language and culture info.
- Implement `CustomImages` class for image management.
- Add `EnvelopeReceiverLogin` record for login requests.
- Introduce `ErrorViewModel` for error representation.
- Add `Image` class for image source and CSS management.
- Implement `Login` record for user authentication.
- Add `MainViewModel` with a nullable `Title` property.
- Introduce PDF annotation classes in `PsPdfKitAnnotation` namespace.
- Add `TFARegParams` class for 2FA registration parameters.
2026-06-22 14:56:03 +02:00
9e37bf1fe2 Enhance authentication and configuration setup
Introduced a new `AuthScheme` class for JWT authentication
schemes. Added `ExceptionHandlingMiddleware` for global
exception handling. Updated `Program.cs` to refactor service
registrations, including Blazor, API controllers, CORS, and
Swagger setup. Removed YARP reverse proxy and added a more
comprehensive configuration for authentication and caching.
Updated `appsettings.json` and `appsettings.Development.json`
with new sections for authentication, logging, and various
application-specific settings. Added new classes for handling
authentication tokens, connection strings, and cache options.
2026-06-22 14:28:43 +02:00
9a0837caa9 Refactor rendering and add PDF resource
Removed `@rendermode="InteractiveAuto"` from `<HeadOutlet />` and `<Routes />` in `App.razor` to adjust rendering mode. Updated `EnvelopeReceiverPage_DxPdfViewer.razor` to use `DevExpress.Blazor.PdfViewer` instead of `DevExpress.Blazor`. Added `@using DevExpress.Blazor` to `_Imports.razor` for project-wide access to DevExpress components. Embedded `Resources\Invoice.pdf` in `EnvelopeGenerator.WebUI.csproj` and added the PDF file to the project.
2026-06-22 10:44:34 +02:00
030646f33d Enhance service configuration and DI setup
Added `EnvelopeGenerator.WebUI.Client.Services` to the using directives. Registered `IHttpContextAccessor` to access HTTP context for request-specific information. Modified `HttpClient` setup to dynamically set the base address using the current request's host. Introduced several business services (`DocumentService`, `AuthService`, `AnnotationService`, `EnvelopeReceiverService`, `SignatureService`, `SignatureCacheService`, `AppVersionService`) to the service collection, indicating new features. Maintained existing YARP configuration. Noted the importance of DevExpress services for `DxPdfViewer`.
2026-06-18 16:15:00 +02:00
88317e40f5 Add AGENTS.md - Quick-start guide for AI agents
- Architecture overview (Blazor Auto Server+WASM hybrid)
- Critical development commands (both API and WebUI must run)
- Route structure with render mode requirements
- Coordinate system conversions (INCHES in DB)
- API architecture quirks and missing endpoints
- Status color coding from legacy VB.NET app
- Common mistakes to avoid
- Configuration locations and migration status
2026-06-18 13:33:21 +02:00
6fe99d0cd0 docs: add culture management migration warnings for Server/Auto
Prepare for Blazor Server/Auto migration by documenting current
WASM-specific culture initialization approach and providing
detailed migration checklist in COPILOT_CONTEXT.md.
2026-06-18 12:50:45 +02:00
45018d04b1 Refactor culture initialization logic
Moved culture initialization from App.razor to Program.cs to ensure culture settings are applied before the app starts. Removed CultureService injection and OnInitializedAsync method from App.razor. Updated LanguageSelector.razor to change language without page reload, enhancing user experience. Added System.Globalization to Program.cs for culture support.
2026-06-18 12:43:56 +02:00
b5af3e61ed Improve language display in LanguageSelector
Replaced the placeholder arrow in the language selector button with the current language name using the new `GetLanguageName` method. Added `GetLanguageName` to map culture codes to language names. Fixed encoding issue by correcting "Fran�ais" to "Français" in the French language option.
2026-06-18 10:34:05 +02:00
314608f27f Fix duplicate root route entry in ReceiverUI docs
Removed a redundant entry for the root route (`/`) in the
`ReceiverUI Route Structure` section of the documentation.
Both entries referenced the same file (`Index.razor`) and
served the same purpose, making the duplication unnecessary.
2026-06-17 17:04:52 +02:00
ba9f233993 Add Language Selector component to footer
Introduced a Language Selector component (`LanguageSelector.razor`) to enable users to switch between German, English, and French. The component includes a dropdown UI with flag icons and language labels, along with logic for toggling the dropdown and changing the app's culture.

Injected `IJSRuntime`, `NavigationManager`, and `CultureService` into the component to handle culture changes and navigation updates.

Updated `MainLayout.razor` to include the Language Selector in the footer. Refactored footer layout in `MainLayout.razor.css` to use flexbox for better alignment and spacing.

Added styles for the Language Selector in `app.css` to ensure a clean and responsive design. Integrated the `flag-icons` library in `index.html` to display flag icons for language representation.
2026-06-17 16:57:39 +02:00
9d962708c4 Initialize culture settings in App.razor
Added dependency injection for `CultureService` in `App.razor` to dynamically initialize culture settings during the component's lifecycle.

Included `System.Globalization` and `EnvelopeGenerator.ReceiverUI.Services` namespaces to support culture-related functionality.

Injected `CultureService` and added an `OnInitializedAsync` method to set `CultureInfo.DefaultThreadCurrentCulture` and `CultureInfo.DefaultThreadCurrentUICulture` based on the service's initialization.

Reformatted surrounding `<Router>` code for clarity.
2026-06-17 16:55:44 +02:00
c93a056ca5 Add CultureService and localization support
Added a new `CultureService` to manage application culture and
localization. The service supports retrieving and setting the
application culture, storing it in `localStorage`, and initializing
it based on stored values, browser settings, or a default fallback.

Registered `CultureService` in the dependency injection container
and added localization support in `Program.cs` using
`builder.Services.AddLocalization()`.
2026-06-17 16:54:40 +02:00
a88a26c248 Remove EGDbContextFactory and related configuration code
The `EGDbContextFactory` class, which implemented the
`IDesignTimeDbContextFactory<EGDbContext>` interface, has been
removed. This includes the `CreateDbContext` method, which handled
design-time DbContext creation by loading configuration from
`appsettings.migration.json`, setting up `DbContextOptions`, and
constructing `DbTriggerParams`.

Additionally, the `#if NET` preprocessor directive and its
corresponding `#endif` have been removed, along with all associated
`using` directives. This change suggests that the factory is no
longer needed due to a shift in the application's architecture or
design-time DbContext handling.
2026-06-17 16:26:24 +02:00
1e963ea215 Add support for JWT authentication
Added `Microsoft.AspNetCore.Authentication.JwtBearer` package to the project to enable JWT authentication. Updated `Program.cs` to include the necessary `using` directive for JWT authentication. No functional changes were made beyond the integration of the new package.
2026-06-17 16:25:57 +02:00
02b857382c Replace SignatureService with DocReceiverElementService
Refactor EnvelopeReceiverPage.razor to inject
DocReceiverElementService instead of SignatureService.

Update Program.cs to remove SignatureService from the
dependency injection configuration and add
DocReceiverElementService as a scoped service.

This change reflects a shift in architecture where
DocReceiverElementService now handles functionality
previously managed by SignatureService.
2026-06-17 16:09:57 +02:00
ca4ec7cb6f Rename SignatureController to DocReceiverElementController
The class `SignatureController` has been renamed to `DocReceiverElementController` to better reflect its purpose. The constructor and its XML documentation have been updated accordingly. The `[Authorize]`, `[ApiController]`, and `[Route]` attributes remain unchanged. A `TODO` comment has been added to indicate future updates for using a signature query.
2026-06-17 16:09:26 +02:00
f2356b3ce4 Refactor SignatureService to DocReceiverElementService
Renamed the `SignatureService` class to `DocReceiverElementService` to reflect its updated purpose. Updated the `GetAsync` method to use the `DocReceiverElement` API endpoint (`/api/DocReceiverElement/{envelopeKey}`) instead of the `Signature` API endpoint. These changes align the service with the new `DocReceiverElement` context.
2026-06-17 16:07:43 +02:00
d61fe79613 Add localization support to the application
Added a package reference for `Microsoft.Extensions.Localization`
to enable localization support. Registered localization services
in `Program.cs` using `builder.Services.AddLocalization()`.
Introduced the `Microsoft.Extensions.Localization` and
`EnvelopeGenerator.Application.Resources` namespaces to support
localized resources. These changes allow the application to
provide content in multiple languages or regions.
2026-06-17 16:05:26 +02:00
714cb9555f feat(i18n): add sender-side UI resources and extension methods
- Added 46 resource strings (de-DE, en-US, fr-FR) for sender UI
- Added corresponding extension methods in Resource.cs
- Migrated from Form app for ReceiverUI sender pages
2026-06-17 15:58:30 +02:00
315a022cb8 feat: Add sender-side UI resource strings for all languages
Added 46 new localized strings to support sender-side UI implementation:

Resources added:
- Resource.de-DE.resx (German)
- Resource.en-US.resx (English)
- Resource.fr-FR.resx (French)

Categories covered:
- Dashboard actions (NewEnvelope, LoadEnvelope, DeleteEnvelope, etc.)
- Grid columns (Receivers, Status, Type, CreatedOn, etc.)
- Toolbar actions (RefreshData, ShowDocument, ResendInvitation, etc.)
- Envelope editor (AddFile, MergeFiles, EditFields, SendEnvelope, etc.)
- Settings (Language, UseAccessCode, TwoFactorEnabled, etc.)
- Reminders & expiry (FirstReminderDays, ExpiresWhenDays, etc.)

Source: Migrated from EnvelopeGenerator.Form resx files
Purpose: Support upcoming ReceiverUI sender pages implementation
2026-06-17 15:55:13 +02:00
746635979b Ensure _allEnvelopes is never null after assignment
Modified the assignment of `_allEnvelopes` to use the result of
`EnvelopeService.GetAsync()` or an empty list (`[]`) if the
result is null. This change prevents potential null reference
issues when `_allEnvelopes` is used later in the code.
2026-06-17 15:10:31 +02:00
31548728cd Refactor EnvelopeStatus to shared domain library
Moved the `EnvelopeStatus` enum and its extension methods
from `ReceiverUI.Models` to `Domain.Constants` to improve
code reuse and maintainability. Updated `EnvelopeSenderPage.razor`
to reference the new namespace.
2026-06-17 15:10:17 +02:00
06c8af2ed8 Remove EnvelopeGenerator.Dto project from solution
The `EnvelopeGenerator.Dto.csproj` file has been removed entirely, including its `<Project>` XML structure.

References to `EnvelopeGenerator.Dto` have been removed from `EnvelopeGenerator.sln`, including the project declaration, build configurations in `SolutionConfigurationPlatforms`, and nested project mappings in `NestedProjects`.

This change fully removes the `EnvelopeGenerator.Dto` project from the solution.
2026-06-17 14:38:30 +02:00
9f57baf2e5 refactor(ReceiverUI/Models): update to use Application layer's DTO 2026-06-17 14:05:01 +02:00
73d793f0a0 remove ApiExplorerSettings attribute 2026-06-17 11:53:01 +02:00
65bb68feef Update DevExpress package versions to 25.2.8
Updated the following DevExpress package references in the project:
- `DevExpress.Blazor.PdfViewer` from 25.2.3 to 25.2.8
- `DevExpress.Blazor.Reporting.JSBasedControls` from 25.2.3 to 25.2.8
- `DevExpress.Blazor.Reporting.Viewer` from 25.2.3 to 25.2.8
- `DevExpress.Drawing.Skia` from 25.2.3 to 25.2.8

No changes were made to other package references or the `NativeFileReference` entry.
2026-06-17 11:05:53 +02:00
c5e97ee30b move dto to common dir 2026-06-17 09:46:27 +02:00
3a4f449b59 Add EnvelopeGenerator.Dto project to the solution
Introduce a new project, `EnvelopeGenerator.Dto`, targeting .NET 8.0.
The project is configured with implicit `using` directives and nullable
reference types enabled. Update the solution file to include the new
project, its build configurations (Debug/Release for Any CPU), and its
hierarchical relationship in the `NestedProjects` section.
2026-06-17 09:45:22 +02:00
6ca7767e4d Enhance grid functionality in EnvelopeSenderPage
Added support for column reordering, sorting, and resizing in the grid:
- Enabled `AllowColumnReorder` for column reordering.
- Enabled `AllowSort` to allow sorting of grid columns.
- Set `ColumnResizeMode` to `GridColumnResizeMode.ColumnsContainer` for improved column resizing behavior.
2026-06-17 09:41:46 +02:00
4237f0a815 Add CellDisplayTemplate for ID column in grid
Updated the `DxGridDataColumn` for the `Id` field in
`EnvelopeSenderPage.razor` to include a `CellDisplayTemplate`.
This enables customized rendering of the `Id` property from
the `EnvelopeDto` object, allowing for additional formatting
or logic during display.
2026-06-16 17:01:38 +02:00
3302be9348 Refactor grid columns and improve UI styling
Removed fixed column widths in `EnvelopeSenderPage.razor` for dynamic sizing. Added `CellDisplayTemplate` to `Title`, `Status`, and `EnvelopeReceivers` columns for custom data rendering. Fixed gradient typo in progress bar CSS.

Added a new CSS rule to hide empty DevExpress grid cells and updated `sender-page.css` to include this rule while preserving existing styles.
2026-06-16 16:47:11 +02:00
4572e20c51 Restrict Logout method to Sender auth scheme
The `[Authorize]` attribute on the `Logout` method in the
`AuthController` class was updated to use the
`AuthenticationSchemes = AuthScheme.Sender` instead of the
`Policy = AuthPolicy.SenderOrReceiver`. This change narrows
the authorization requirement, ensuring only users under the
`Sender` authentication scheme can access the `Logout`
functionality.
2026-06-16 16:32:53 +02:00
b3a70d7259 Add sender authentication check to EnvelopeSenderPage
Added an authentication check in `EnvelopeSenderPage.razor` to verify sender access before loading envelopes. Redirects unauthorized users to the sender login page.

Introduced `CheckSenderAsync` in `AuthService` to validate sender tokens via the `/api/auth/check` endpoint. Updated `OnInitializedAsync` to use this method, enhancing security by ensuring only authorized users can access envelope-related functionality.
2026-06-16 15:55:59 +02:00
bb81920d44 Refactor sender page styles and add versioned URLs
Moved inline styles from `EnvelopeSenderPage.razor` to a new
`sender-page.css` file for better maintainability and separation
of concerns. Updated `EnvelopeSenderPage.razor` to use versioned
URLs for stylesheets via the newly injected `AppVersionService`,
enabling cache-busting. Added responsive design support in
`sender-page.css` to improve layout on smaller screens.
2026-06-16 15:05:00 +02:00
3b66de0691 Enhance EnvelopeSenderPage with new UI and features
Integrated DevExpress Blazor components and added a responsive,
modern UI for the sender dashboard. Replaced placeholder content
with a functional layout, including a grid-based envelope viewer
with filtering, pagination, and detailed row templates.

Added status badges, progress indicators, and a sender action
bar with buttons for creating, editing, deleting, refreshing
envelopes, and logging out. Introduced loading and error
handling states for better user experience.

Refactored data loading with `LoadEnvelopesAsync` to fetch and
categorize envelopes. Added methods for envelope management
and logout functionality. Improved state management and removed
unused code. These changes lay the groundwork for future
enhancements.
2026-06-15 17:00:23 +02:00
9f6004ba8c Refactor EnvelopeDto and add EnvelopeStatus enum
Updated the `EnvelopeDto` class to use a simplified receiver model (`EnvelopeReceiverSimpleDto`) for streamlined data handling. Added the `EnvelopeReceiverSimpleDto` class to represent basic receiver information (`Name`, `Email`, `Signed`).

Introduced the `EnvelopeStatus` enumeration in `EnvelopeStatus.cs` to define envelope lifecycle statuses, repurposed for the `ReceiverUI` context. Added `EnvelopeStatusExtensions` with `IsActive` and `IsCompleted` methods to evaluate envelope status states.
2026-06-15 16:59:37 +02:00
ef246bae32 Add LogoutSenderAsync method to AuthService
A new asynchronous method `LogoutSenderAsync` was added to the `AuthService` class to handle sender user logout. The method sends a POST request to the `/api/auth/logout` endpoint and removes the authentication cookie. It accepts an optional `CancellationToken` parameter and returns a `bool` indicating the success of the operation. XML documentation comments were included to describe the method's functionality.
2026-06-15 16:59:16 +02:00
e4ebb29969 Add authorization and data fetching to sender page
Added an `[Authorize]` attribute with the "Sender" policy to restrict access to the `EnvelopeSenderPage.razor`. Updated the page title to "Umschläge" and added placeholder text for data loading.

Injected `EnvelopeService` and `IJSRuntime` to fetch and log active and completed envelopes. Introduced `_activeEnvelopes` and `_completedEnvelopes` fields to store fetched data. Configured `JsonSerializerOptions` for consistent JSON handling.

Implemented `OnInitializedAsync` to fetch data asynchronously, log results to the console, and handle errors gracefully.
2026-06-15 16:24:45 +02:00
83cdb9dfe9 Update launch settings for HTTPS and ReceiverUI profiles
Changed the `launchUrl` for the HTTPS profile in `launchSettings.json` from `"swagger"` to `"sender"`, updating the default URL path.
Disabled `launchBrowser` for the `EnvelopeGenerator.ReceiverUI` profile, preventing the browser from automatically opening when this profile is executed.
2026-06-15 16:20:28 +02:00
c5db676e01 Add EnvelopeService to DI container in Program.cs
Registered EnvelopeService with a scoped lifetime in the
dependency injection container by adding
`builder.Services.AddScoped<EnvelopeService>();` to Program.cs.
This ensures a new instance is created per HTTP request.
2026-06-15 15:56:49 +02:00
95c8e15887 Add EnvelopeDto and EnvelopeService for API integration
Introduced the `EnvelopeDto` class to represent envelope data with JSON property mappings. Added the `EnvelopeService` class to handle API interactions, including fetching envelopes with optional filters and query string construction using `Microsoft.AspNetCore.WebUtilities`. Updated the project file to include the required package reference for query string manipulation.
2026-06-15 15:40:59 +02:00
561b844e59 Add filtering for active and completed envelopes
Added `OnlyActive` and `OnlyCompleted` properties to the `ReadEnvelopeQuery` class to enable filtering envelopes by their active or completed status. Updated the `ReadEnvelopeQueryHandler` to apply these filters when the properties are set.

Enhanced the `EnvelopeStatus` class by introducing `Active` and `Completed` status lists and adding extension methods (`IsActive` and `IsCompleted`) to determine status categories. Included necessary `using` directives for `System` and `System.Linq`.
2026-06-15 15:07:12 +02:00
011960be75 Add EnvelopeStatus extensions and update documentation URL
The `System` namespace was added to `EnvelopeStatus.cs` to enable additional functionality. A documentation URL in the comments was updated to point to a new location, replacing the outdated link.

Introduced a new static class `EnvelopeStatusExtensions` with two extension methods for the `EnvelopeStatus` enum:
- `IsActive`: Checks if the status is active (between `EnvelopeCreated` and `EnvelopePartlySigned`).
- `IsCompleted`: Checks if the status is completed (between `EnvelopeCompletelySigned` and `EnvelopeWithdrawn`).
2026-06-15 14:55:01 +02:00
3a2fa77862 refactor(WebUI): configure HtpClient 2026-06-15 11:33:03 +02:00
cfa6dbd2de remove deprecated parameter 2026-06-15 11:18:34 +02:00
eb2603f389 remove wwwroot/app.css and add js and css dependencies 2026-06-15 11:04:35 +02:00
456178bee1 migrate shared components 2026-06-15 10:54:08 +02:00
2c41c74510 refactor(COPILOT_CONTEXT): update to be compatible with the migration from ReceiverUI to WebUI 2026-06-15 10:44:19 +02:00
bb73795d68 remove MIGRATION_CONTEXT.md
Migrate ReceiverUI to hybrid Blazor WebUI architecture

Migrated the `EnvelopeGenerator.ReceiverUI` project to a new
hybrid Blazor architecture (`EnvelopeGenerator.WebUI`) that
supports both Blazor Server and WebAssembly (WASM) modes.

- Added `WebUI` (server) and `WebUI.Client` (WASM) projects.
- Migrated client-side pages to `WebUI.Client` with
  `@rendermode InteractiveWebAssembly`.
- Migrated server-side pages to `WebUI` with
  `@rendermode InteractiveServer`.
- Added YARP reverse proxy (`yarp.json`) to `WebUI` for API
  and Swagger routing.
- Registered DevExpress server-side services in `WebUI` to
  enable backend rendering for `DxPdfViewer`.
- Migrated services, models, options, and data files to
  `WebUI.Client` with updated namespaces.
- Merged static files (JS, CSS, configuration) from
  `ReceiverUI/wwwroot` to `WebUI/wwwroot`.
- Retained `ReceiverUI` project for rollback safety.

This migration resolves the issue where the DevExpress
`DxPdfViewer` failed to render PDFs in a pure Blazor
WebAssembly environment due to missing server-side rendering
services.
2026-06-15 10:41:53 +02:00
207992d95a fix(WebUI.Client.Services): resolve doc comment icons 2026-06-15 10:27:51 +02:00
d6bafc64a6 Fix BOM issue in using directives in Report.cs and ReportsFactory.cs
The `using DevExpress.XtraReports.UI;` directive was modified in
both `Report.cs` and `ReportsFactory.cs` due to the addition of a
non-visible character (likely a Byte Order Mark or BOM) at the
beginning of the line. This change does not affect functionality
but may resolve potential issues with tools or version control
systems sensitive to such characters.
2026-06-15 10:18:31 +02:00
3090711892 resolve comment icons 2026-06-15 10:13:22 +02:00
9dbd8f7952 remove Home.razor 2026-06-15 09:59:42 +02:00
48a41f2987 refactor(wwwroot): move docs, fonts, fake-data, images and js files.
- update appsettings.json to fix
2026-06-15 09:59:18 +02:00
96688a951c refaactor: move css files to WebUI 2026-06-15 09:50:19 +02:00
6f07de3ec4 refactor(Pages): set the render mode of SSR pages as InteractiveServer 2026-06-15 09:40:41 +02:00
4611266224 fix(WebUI): resove referances 2026-06-15 09:30:00 +02:00
c529d03129 move ssr pages 2026-06-15 09:26:00 +02:00
829fab9647 Add PDF viewer, signature, and typing enhancements
Enhanced `appsettings.json` with `ApiOptions` and `PdfViewerOptions` for better customization of rendering and zoom behaviors.

Added comprehensive styles in `envelope-viewer.css` for the layout, toolbar, thumbnails, and responsive design of the envelope viewer.

Introduced `receiver-signature.js` to manage signature functionality, including drawing, typed, and image-based signatures, as well as dynamic annotation checkboxes.

Integrated `Typed.js` (UMD version) for typing animations with configurable speeds, looping, and event hooks.
2026-06-15 09:20:12 +02:00
b2e3605b54 migrate pdf-viewer.js 2026-06-12 22:15:15 +02:00
8cbdee2491 Integrate DevExpress and enhance app services
Added DevExpress WASM components for PDF and report viewing, including Blazor PDF Viewer and Report Viewer. Configured DevExpress Blazor Reporting for development mode and registered custom reporting services and trusted classes for deserialization.

Replaced default `HttpClient` with a scoped service using WebUI's YARP proxy. Introduced configuration options for `ApiOptions` and `PdfViewerOptions` and registered multiple business services in the DI container.

Added in-memory report storage and font loading functionality. Updated `_Imports.razor` with additional namespaces. Re-enabled and implemented previously commented-out configuration options and removed obsolete code.
2026-06-12 21:52:52 +02:00
151c785af9 Enhance JSON options and authorization policies
Added JSON serialization options to ignore reference cycles in
the `AddControllers` method by configuring `ReferenceHandler`
to `IgnoreCycles`. Updated the `AddAuthorizationBuilder` to
include authentication schemes for the `SenderOrReceiver`
policy, requiring roles and schemes for enhanced security.
2026-06-12 15:21:50 +02:00
fa354a05cc Update authorization policy in ConfigController
Replaced the generic [Authorize] attribute with a more specific
[Authorize(Policy = AuthPolicy.SenderOrReceiver)] to enforce
a stricter authorization policy. Added a `using` directive for
`EnvelopeGenerator.Domain.Constants` to support the new policy.
2026-06-12 15:21:29 +02:00
1326407462 Update AuthController to use specific auth scheme
The `[Authorize]` attribute on the `Check` method was updated to specify the `AuthScheme.Sender` authentication scheme. This change ensures that the `Check` endpoint now requires authentication using this specific scheme, enhancing security and supporting multiple authentication schemes within the application.
2026-06-12 15:15:44 +02:00
a3c653ddb3 Simplify Envelope to EnvelopeDto mapping
Removed custom mapping logic for the `Receivers` property in the
`Envelope` to `EnvelopeDto` mapping within the `MappingProfile`
class. The mapping now uses default behavior without projecting
`EnvelopeReceivers` to `Receivers`.
2026-06-12 15:15:09 +02:00
8d736cdc5e Refactor EnvelopeDto property for receiver handling
Replaced the `Receivers` property with `EnvelopeReceivers` in the `EnvelopeDto` class to improve clarity and better align with the updated data model. The new property uses `IEnumerable<EnvelopeReceiverDto>?` instead of `IEnumerable<ReceiverDto>?`.
2026-06-12 15:14:51 +02:00
4281eaeb22 migrate predefined report to WebUI 2026-06-12 14:46:22 +02:00
150fca5f47 Add data models, randomization, and report factory setup
Introduced several new classes in the `EnvelopeGenerator.WebUI.Client` namespace:
- Added `Adjustment` class for financial adjustments with deterministic randomization.
- Added `Customer` class to load customer data from a SQL data source with fallback.
- Added `DataItem` class to represent detailed billing data, including adjustments.
- Added `DataItemList` class implementing `IList` for dynamic `DataItem` generation.
- Added `DeterministicRandom` class for reproducible random value generation.
- Added `Term` struct to define payment terms.
- Added `ReportsFactory` class to manage predefined reports.

Updated `MIGRATION_CONTEXT.md` to document the completion of Phase 5 (Data & PredefinedReports Migration) and outline next steps for resolving DevExpress-related errors in Phase 7.
2026-06-12 14:04:37 +02:00
1f889d8b58 Add signature workflow models, services, and configurations
Introduced new models (`SignatureDto`, `SignatureCaptureDto`, `EnvelopeReceiverDto`) to support a signature-based workflow. Added services for handling API interactions (`SignatureService`, `AuthService`, `DocumentService`, `EnvelopeReceiverService`, `SignatureCacheService`).

Enhanced configuration with `ApiOptions` and `PdfViewerOptions`. Integrated DevExpress features with custom data connection providers, in-memory report storage, and font loading utilities.

Marked `AnnotationDto` and `AnnotationService` as `[Obsolete]` in favor of newer implementations. Added detailed documentation for coordinate systems, unit conversions, and usage scenarios.
2026-06-12 14:02:55 +02:00
d599fe3156 Migrate initial YARP setup and client-side pages
- Added `yarp.json` for reverse proxy configuration.
- Updated `WebUI.csproj` with YARP and DevExpress packages.
- Modified `Program.cs` to load YARP config and register services.
- Migrated 4 client-side pages with `@rendermode InteractiveWebAssembly`.
- Migrated 13 services, 7 models, and 2 options classes.
- Updated namespaces to `EnvelopeGenerator.WebUI.Client.*`.
- Documented 43 expected DevExpress-related build errors.
- Pending migration of predefined reports and missing NuGet packages.
2026-06-12 13:56:12 +02:00
6c40c48ac8 Add pages for sender/receiver login and homepage UI
Added `EnvelopeSenderPage.razor` as a placeholder for the sender's dashboard.
Updated `Index.razor` to include a homepage with a hero header, feature badges,
and dynamic description rendering using JavaScript interop.

Implemented `LoginReceiverPage.razor` for secure document access via access code,
with error handling and user feedback for various login states.

Implemented `LoginSenderPage.razor` for sender authentication, including error
handling, password visibility toggle, and redirection to the sender dashboard.
2026-06-12 13:25:29 +02:00
536b8ef5da Integrate YARP and DevExpress Blazor components
Added YARP reverse proxy for API routing and DevExpress Blazor components for advanced UI features, including PDF Viewer. Updated `EnvelopeGenerator.WebUI.csproj` to include necessary packages and ensure `yarp.json` is copied to the output directory. Modified `Program.cs` to configure YARP and DevExpress services, and adjusted the HTTP pipeline for proper routing. Updated `appsettings.json` with `ApiOptions` and `PdfViewerOptions`. Added `yarp.json` to define reverse proxy routes and clusters.
2026-06-12 13:10:17 +02:00
d35a35c75e feat(webui): migrate to hybrid Blazor architecture
Migrated `EnvelopeGenerator.ReceiverUI` to a new hybrid
Blazor architecture (`EnvelopeGenerator.WebUI`) combining
Blazor Server and WebAssembly modes. This resolves the
issue with `DxPdfViewer` requiring server-side rendering.

Key changes:
- Introduced `WebUI` (Blazor Server) and `WebUI.Client`
  (Blazor WebAssembly) projects.
- Added YARP reverse proxy to `WebUI` for API routing.
- Migrated client-side pages to `WebUI.Client` with
  `@rendermode InteractiveWebAssembly`.
- Migrated server-side pages (e.g., PDF viewer) to `WebUI`
  with `@rendermode InteractiveServer`.
- Copied services, models, and static files from `ReceiverUI`.
- Configured DevExpress server-side and WASM components.

Includes detailed migration documentation, rollback plan,
and testing strategies to ensure stability.
2026-06-12 12:48:32 +02:00
7fb1a87cf2 Add Blazor WebAssembly and Server projects
Introduced a new Blazor WebAssembly project (`EnvelopeGenerator.WebUI.Client`) targeting .NET 8.0 for client-side functionality.

Added a Blazor Server project (`EnvelopeGenerator.WebUI`) to host the application and enable server-side rendering.

Created essential Razor components (`MainLayout.razor`, `Home.razor`, `Routes.razor`, `Error.razor`, etc.) for layouts, routing, and error handling.

Configured project files, solution structure, and build settings. Added necessary styles, app settings, and launch profiles for development.
2026-06-12 12:25:24 +02:00
a3b33637fd Update authorization scheme for GetAsync method
Modified the `[Authorize]` attribute on the `GetAsync` method in the `EnvelopeController` class to specify `AuthenticationSchemes = AuthScheme.Sender`. This change enforces a more specific authentication requirement, allowing access only to users authenticated under the `Sender` scheme.
2026-06-11 23:04:53 +02:00
bc3134a033 Update Envelope to EnvelopeDto mapping configuration
The mapping for the `Envelope` entity to the `EnvelopeDto` was
modified to include a custom mapping for the `Receivers` property.
The `Receivers` property in `EnvelopeDto` is now populated by
mapping from the `EnvelopeReceivers` collection in the `Envelope`
entity, specifically selecting the `Receiver` property from each
`EnvelopeReceiver` object.
2026-06-11 23:04:43 +02:00
f106255c6b Add Receivers property to EnvelopeDto
Enhanced the EnvelopeDto class by introducing a new `Receivers` property of type `IEnumerable<ReceiverDto>?` to support including recipient information in the envelope DTO.

Added necessary `using` directives for `EnvelopeReceiver` and `Receiver` DTOs to ensure proper namespace references. Updated the class to accommodate this new functionality.
2026-06-11 23:04:31 +02:00
cb103dcb69 Restrict UserId visibility and update query includes
The `UserId` property in `ReadEnvelopeQuery` was changed from `public` to `internal` to improve encapsulation. A new `Authorize` method was added to set the `UserId` property using the `with` expression.

In `ReadEnvelopeQueryHandler`, the LINQ query was updated to replace the inclusion of `Documents` with `EnvelopeReceivers` and their associated `Receiver`, reflecting a shift in the data being eagerly loaded to support updated functionality.
2026-06-11 23:04:05 +02:00
8c1dd9c40d Make boolean properties nullable in Envelope class
Changed `SendReminderEmails`, `UseAccessCode`, and `TfaEnabled`
properties from non-nullable `bool` to nullable `bool?` to allow
representation of `null` values. Added conditional initialization
of `TfaEnabled` to `false` for `NETFRAMEWORK` target.
2026-06-11 23:03:35 +02:00
ee358ffaab Update project version to 1.4.2
Bump the project version from 1.4.1 to 1.4.2 in the `<Version>` tag for the NuGet package. Updated `<AssemblyVersion>` and `<FileVersion>` to `1.4.2.0` to maintain consistency with the new version. This reflects a minor version update for API compatibility and file versioning.
2026-06-11 17:14:20 +02:00
0780dbdd94 Enhance PDF viewer and add embed page with file upload
- Added CSS styles for `.pdf-viewer` and its child elements
  to ensure proper dimensions and layout for PDF display.
- Enhanced `EnvelopeReceiverPage_DxPdfViewer.razor` with
  conditional rendering for improved user feedback.
- Introduced `EnvelopeReceiverPage_embed.razor` with a new
  route `/envelope/Embed`, drag-and-drop file upload, and
  embedded PDF viewer using `<embed>`.
- Implemented default PDF loading from embedded resources
  and Base64 encoding for embedding.
- Refactored file upload handling with `OnFilesUploading`
  and centralized allowed file types logic.
- Improved user experience with success and informational
  messages for file upload and PDF viewing.
2026-06-11 17:13:17 +02:00
d722742fe8 Remove unused DevExpress Reporting CSS file
The `dx-blazor-reporting-components.bs5.css` file reference was removed from `EnvelopeReceiverPage_DxPdfViewer.razor`. This CSS file was likely used for styling DevExpress Blazor Reporting Viewer components, which are no longer needed or have been replaced.
2026-06-11 15:55:26 +02:00
8c42105f58 Add PDF viewer with drag-and-drop file upload support
Added a new Razor page `EnvelopeReceiverPage_DxPdfViewer.razor` with a route `/envelope/DxPdfViewer`. Integrated DevExpress components, including `DxPdfViewer` for displaying PDF documents and `DxFileInput` for drag-and-drop file uploads. Styled the drag-and-drop zone with custom CSS. Initialized the viewer with a default embedded PDF file and implemented logic to handle file uploads dynamically.
2026-06-11 15:33:22 +02:00
88b196ed6d Refactor report handling and improve async operations
Refactored the `Report` property to `_report` with nullable support
and updated `EnvelopeKey` to use `init` for immutability.
Made `CreateReport` asynchronous, returning `Task<XtraReport>`,
and removed redundant `BasePdfBytes` property. Simplified predefined
report fetching by removing `ReportStorage.TryGetReport`. Improved
error handling for missing or invalid `pdfBytes` in `CreateReport`.
Made minor formatting and structural improvements for clarity.
2026-06-11 14:11:56 +02:00
c99511de29 Add FORM_APPLICATION_CONTEXT.md for migration documentation
Added a new file, `FORM_APPLICATION_CONTEXT.md`, to the solution file `EnvelopeGenerator.sln`. This file provides detailed documentation of the legacy VB.NET Windows Forms application, including its architecture, workflows, and migration plan to the ReceiverUI + API architecture.

The documentation outlines the application's purpose, primary libraries, project structure, and key forms with their respective features and behaviors. It also describes data models, controllers, and their methods, along with technical details such as coordinate systems, status colors, and master-detail grid patterns.

Additionally, the file includes implementation notes for key features like drag-and-drop file uploads, auto-complete for receiver emails, and PDF merging. Workflow summaries and a data flow diagram are provided to clarify the application's processes. The document concludes with key takeaways to ensure consistency during the migration.
2026-06-11 13:52:55 +02:00
7d0c5a0ee5 Improve error handling and logging for envelope receiver
Added a null check in `EnvelopeReceiverPage.razor` to log a warning when `_envelopeReceiver` is null. Updated `ReportViewer.razor` to wrap `EnvelopeReceiverService.GetAsync` in a `try-catch` block, logging `HttpRequestException` errors and allowing the UI to handle null values gracefully.

Enhanced `EnvelopeReceiverService.GetAsync` to throw detailed `HttpRequestException` on API failures, including status code and reason phrase. Added `using System.Net;` to support HTTP-related classes. Updated method documentation to reflect the new behavior.

These changes improve error diagnostics, logging, and maintainability across the codebase.
2026-06-11 13:40:30 +02:00
7001d7351f Remove default "fake" fallback for EnvelopeKey usage
The default value "fake" for the `EnvelopeKey` parameter has been
removed when calling `AnnotationService.GetAnnotationsAsync` and
`EnvelopeReceiverService.GetAsync`. The code now directly uses
the `EnvelopeKey` variable, assuming it will always have a valid
value or that null/empty handling is managed elsewhere.
2026-06-11 13:37:55 +02:00
cf16312394 Refactor error handling in DocumentService and consumers
Refactored `DocumentService.GetDocumentAsync` to throw
`HttpRequestException` on failure instead of returning a tuple.
Updated all consumers to handle exceptions, improving error
handling consistency across the application.

Enhanced error messages for better user feedback and added
logging for improved traceability. Updated `EnvelopeReceiverPage`,
`EnvelopeReceiverPage_DxReportViewer`, and `ReportViewer` to
use the new exception-based API and handle errors gracefully.

This change simplifies the API, improves maintainability, and
makes the application more robust and user-friendly.
2026-06-11 13:33:15 +02:00
ecb7f45f14 Refactor report creation and property initialization
Replaced nullable fields with non-nullable properties for `Report`
and `BasePdfBytes` to improve initialization and encapsulation.
Updated `OnInitializedAsync` to use `BasePdfBytes` and added
error handling for missing PDF bytes.

Refactored report creation logic by removing `BuildFreshBaseReport`
and `CreateReportInstance`, consolidating functionality into a
new `CreateReport` method. Enhanced `CreateReport` to support
both predefined and dynamically generated reports, simplifying
the code structure and improving maintainability.
2026-06-11 13:12:40 +02:00
0ba5578d94 Update appsettings.json configuration options
Removed the `ForceToUseFakeDocument` property from the `Api` section, as it is no longer needed. Added a new property, `UsePredefinedReports`, to the `Api` section with a default value of `false` to enable or disable the use of predefined reports.
2026-06-11 13:11:51 +02:00
e093471a24 Refactor: Replace ForceToUseFakeDocument property
Replaced the `ForceToUseFakeDocument` property in `ApiOptions`
with `UsePredefinedReports` to improve clarity and better align
with business requirements. Updated all references in
`EnvelopeReceiverPage_DxReportViewer.razor` and
`ReportViewer.razor` to use the new property, ensuring
consistency and maintaining functionality.
2026-06-11 13:07:17 +02:00
b16ae70762 Remove signature-related functionality
This commit removes all features and UI elements related to signature handling in the `EnvelopeReceiverPage_DxReportViewer.razor` file.

Key changes include:
- Removed signature creation (drawing, typing, uploading) and associated UI components like the signature popup and action bar.
- Removed annotation handling logic, including toggling checkboxes and applying signatures to annotations.
- Removed PDF export functionality for signed documents.
- Deleted methods and properties related to signature handling, such as `AddSignature`, `OnInitializedAsync`, and `SignaturePopupVisible`.
- Removed `@inject` services and JavaScript interop calls related to signature handling.
- Simplified the `DxReportViewer` usage to only display the report without overlays or interactions.
- Removed envelope metadata display and logout functionality.

The page now focuses solely on displaying reports without any signature-related features.
2026-06-11 13:02:53 +02:00
c3e8f09291 Simplify EnvelopeReceiverPage logic and update settings
Removed redundant `EnvelopeKey` checks in `LogoutAsync` and
`OnInitializedAsync` methods, simplifying logout and initialization
logic. Updated navigation URL to `/envelope/login/`. Streamlined
document fetching logic by removing unnecessary conditions.

Added logging in `CreateReportInstance` to track report creation.
Enabled `ForceToUseFakeDocument` in `appsettings.json` to default
to using fake documents. These changes improve code clarity and
align behavior with updated requirements.
2026-06-11 12:51:04 +02:00
a9fb82bbea Refactor _dotNetRef type and remove unused redirection
Updated the `_dotNetRef` field to use a more specific type, `DotNetObjectReference<EnvelopeReceiverPage_DxReportViewer>`, for better alignment with the component. Removed obsolete redirection logic from `/receiver/{key}` to `/envelope/{key}` in `OnInitializedAsync`, as it is no longer needed or handled elsewhere. Retained envelope access validation logic.
2026-06-11 11:59:21 +02:00
895fd5c509 Add route for DxReportViewer in yarp.json
Introduce a new route configuration for `receiver-ui-envelope-dxreportviewer` in `yarp.json`.
The route matches requests to `/envelope/{EnvelopeKey}/DxReportViewer` for `GET` and `HEAD` methods.
It is part of the `receiver-ui` cluster, with an order of `90`, and includes a transformation to redirect the path to `/index.html`.
This change supports a new endpoint in the application.
2026-06-11 11:58:58 +02:00
3b4278d7e0 Add envelope viewer with signature functionality
Introduced a new Razor page `EnvelopeReceiverPage_DxReportViewer.razor` to manage and sign envelope-related documents. Integrated DevExpress Blazor components for PDF rendering and signature handling.

Key features:
- Added sections for envelope info, signature actions, and a PDF viewer.
- Enabled signature creation via drawing, text input, or image upload.
- Validated and applied signatures to annotated fields in the document.
- Integrated JavaScript interop for signature capture and rendering.
- Supported exporting signed PDFs and dynamic signature overlays.
- Ensured proper resource cleanup with `IDisposable` implementation.
2026-06-11 11:52:11 +02:00
6d1fb05e10 Update solution items in EnvelopeGenerator.sln
Replaced `COPILOT_CONTEXT_EN.md` with `COPILOT_CONTEXT.md`
in the `ProjectSection(SolutionItems)` of the solution file
`EnvelopeGenerator.sln` to reflect updated documentation.
2026-06-11 11:38:59 +02:00
26da78fa22 Refactor ReceiverUI: Rename files, update routes
Renamed Razor component files in ReceiverUI to follow a consistent "Page" naming convention. Updated routes to reference the renamed files. Introduced the root route (`/`) with `Index.razor` as the application entry point.

Updated documentation to reflect the file renames, route changes, and multi-envelope support. Clarified Redis/SQL caching details and deprecated the "Web" frontend in favor of `ReceiverUI`.

These changes improve maintainability, consistency, and developer experience.
2026-06-11 11:38:32 +02:00
9eee2b523d Refactor YARP routes and update configurations
Renamed the `receiver-ui-receiver` route to `receiver-ui-root` and updated its `Order` value from `100` to `300`. Changed the `Match` path for `receiver-ui-root` from `/receiver/{**catch-all}` to `/`. Removed the `receiver-ui-login` route entirely.

Added a `Transforms` section to the `receiver-ui-root` and `receiver-ui-sender` routes, setting the path to `/index.html`.
2026-06-11 11:38:19 +02:00
9dc2b9adef rename Pages\*.razor to Pages\*Page.razor to resolve conflicts 2026-06-11 11:07:32 +02:00
cc3c5ec9f0 Unify architecture with Blazor WASM and YARP proxy
Refactored the deployment architecture to include two distinct
presentation projects: `EnvelopeGenerator.API` (ASP.NET Core Web
API with YARP Reverse Proxy) and `EnvelopeGenerator.ReceiverUI`
(Blazor WebAssembly). The API now acts as the single entry point,
proxying requests to the ReceiverUI and external authentication
services.

Redefined the route structure for clarity, introducing sender
routes (`/sender/login`, `/sender`) and receiver routes
(`/envelope/login/{EnvelopeKey}`, `/envelope/{EnvelopeKey}`).
Added multi-envelope support with per-envelope cookies for
simultaneous authentication.

Renamed `EnvelopeViewer` to `EnvelopeReceiver` to reflect its
expanded functionality for viewing and signing envelopes.
Replaced iText7 and PSPDFKit with PDF.js 3.11.174 for document
viewing and signing, with configurable quality settings.

Updated `AuthService` and `SignatureCacheService` to support the
new route structure and multi-envelope authentication. Adjusted
sender login flow to redirect to `/sender` upon success.

Updated documentation to reflect the new architecture, route
structure, and file renames. Deprecated libraries and past
mistakes were documented to avoid repetition.
2026-06-11 11:02:01 +02:00
2766d963af Update login route and refactor _dotNetRef type
Updated the login route from `/login/{EnvelopeKey}` to
`/envelope/login/{EnvelopeKey}` across the application, including
navigation paths in `EnvelopeReceiver.razor` and the `@page` directive
in `LoginReceiver.razor`.

Refactored `_dotNetRef` in `EnvelopeReceiver.razor` to use
`DotNetObjectReference<enveloperece>?` instead of
`DotNetObjectReference<EnvelopeReceiver>?` to reflect a change in the
referenced type.
2026-06-11 10:59:50 +02:00
8fd9928524 Update routes and add EnvelopeSender component
Added a new `EnvelopeSender.razor` component with the route `/sender`.
Updated the route for `LoginSender.razor` from `/login` to `/sender/login`.
Modified navigation logic in `LoginSender.razor` to redirect to
`/sender` after a successful login instead of the root route.
2026-06-11 10:50:28 +02:00
fb3ee14f8f Initialize _pdfLoaded and update _dotNetRef type
The `_pdfLoaded` variable was initialized to `false` in the `EnvelopeReceiver.razor` file. Additionally, the type of `_dotNetRef` was changed from `DotNetObjectReference<EnvelopeViewer>?` to `DotNetObjectReference<EnvelopeReceiver>?` to reflect the correct object reference.
2026-06-11 10:49:31 +02:00
e3929a99e3 rename EnvelopeViewer to EnvelopeReceiver 2026-06-11 10:48:03 +02:00
b6d86aa3eb Refactor documentation for unified architecture
Updated documentation to reflect the transition to a unified Blazor WASM frontend for both Senders and Receivers. Removed references to PSPDFKit and legacy components, emphasizing the use of PDF.js and DevExpress.

Key changes include:
- Revised purpose and architecture sections.
- Updated solution structure to mark `EnvelopeGenerator.Web` as deprecated.
- Enhanced coordinate system explanation with conversion formulas.
- Documented new `EnvelopeViewer` features and signature workflows.
- Added details on signature caching and login flows for Senders and Receivers.
- Expanded "Mistakes History" to highlight lessons learned.
- Added quick reference for debugging and development consistency.

These changes improve clarity, maintainability, and alignment with the current system architecture.
2026-06-11 10:25:44 +02:00
4171a3138b Add LoginSender.razor for Sender user authentication
Implemented a new `LoginSender.razor` component to provide a login page for "Sender" users. The page includes:
- A responsive card layout with a header, input fields for "Username" and "Password," and a submit button with a loading spinner.
- Error handling for invalid credentials and server errors, with appropriate alerts.
- A password visibility toggle for better user experience.
- Integration with `AuthService` for authentication and `NavigationManager` for redirection.

Added Blazor code-behind logic to manage state, handle login submission, and trigger login on "Enter" key press. The page is styled with a custom theme and includes a footer for support information.
2026-06-11 10:09:59 +02:00
e98e18cfe0 Add sender login functionality to AuthService
Added a `SenderLoginResult` enum to represent outcomes of sender login attempts.
Implemented the `LoginSenderAsync` method in `AuthService` to authenticate sender users via the `/api/auth?cookie=true` endpoint.
The method handles HTTP response codes and returns appropriate `SenderLoginResult` values.
Included the `System.Net.Http.Json` namespace to support JSON-based HTTP requests.
2026-06-11 10:01:33 +02:00
14aff03de4 rename Login.razor as LoginReceiver.razor 2026-06-11 09:59:58 +02:00
d828a5bfe2 Refactor authentication schemes for sender and receiver
Updated `AuthScheme` to introduce a distinct `Sender` scheme
and renamed the `Receiver` scheme for clarity. Updated
`Program.cs` to use the new `Sender` scheme in JWT
authentication and explicitly associate authentication
schemes with `Sender` and `Receiver` policies. Removed the
deprecated `AuthPolicy.ReceiverTFA` policy. These changes
improve the separation and maintainability of authentication
and authorization logic.
2026-06-10 22:25:02 +02:00
a6e174e7c1 Refactor JWT auth scheme configuration
Replaced hardcoded per-envelope receiver JWT auth scheme string with a new `AuthScheme` static class containing a `Receiver` constant. Updated `Program.cs` to use `AuthScheme.Receiver` for authentication and policy configuration. Removed redundant comments and unused constants. Added necessary `using` directive for `AuthScheme`.
2026-06-10 17:14:46 +02:00
fc7aa83513 Refactor authorization policies in Program.cs
Reformatted the `AddAuthorizationBuilder()` method for improved readability and consistency. Updated `AuthPolicy.Receiver` to include an additional role, `"receiver"`, in the `RequireRole` method. No functional changes were made to other policies. These changes enhance code maintainability and introduce a minor adjustment to the `AuthPolicy.Receiver` policy.
2026-06-10 15:39:57 +02:00
90661cb856 Enhance SaveSignatureBehavior with validation and repo logic
Updated SaveSignatureBehavior.cs to improve functionality:
- Added new dependencies: AutoMapper, IRepository, and EF Core.
- Enhanced constructor to initialize new dependencies.
- Updated Handle method to validate signatures and handle errors.
- Introduced repository operations for querying and updating elements.
- Improved error handling with BadRequestException.
- Cleaned up redundant code and resolved namespace conflicts.
2026-06-10 15:18:53 +02:00
04e30b0d79 Add ReceiverAppType property and enum to SigningCommand
Added a new `ReceiverAppType` property to the `SigningCommand`
record, initialized to `ReceiverAppType.ReceiverUI`. Introduced
a `ReceiverAppType` enum with values `ReceiverUI` and `LegacyWeb`.
Updated `SignCommandHandler` to reflect these changes without
modifying its functionality.
2026-06-10 15:18:22 +02:00
d0a50f63db Improve validation and error handling in AnnotationBehavior
Updated `using` directives to include `DigitalData.Core.Exceptions` for enhanced exception handling. Updated the `[Obsolete]` attribute message to reflect PSPDFKit library deprecation. Renamed `cancellationToken` to `cancel` for consistency.

Added validation to ensure `PsPdfKitAnnotation` is only supported for the `LegacyWeb` receiver type. Introduced stricter checks for missing or invalid annotation data, throwing `BadRequestException` when necessary. Updated `await next` calls to use the renamed parameter.

These changes improve code clarity, enforce stricter validation, and enhance error handling.
2026-06-10 15:17:25 +02:00
9d20ba1987 Rename ElementId to Id in Signature record
The `ElementId` property in the `Signature` record was renamed to `Id`. This change simplifies the property name, making it more concise and aligning with standard naming conventions or domain terminology.
2026-06-10 12:38:38 +02:00
66f7b6f5e1 Enhance AutoMapper mappings and add base64 decoding
Updated `MappingProfile` to map `Signature.DataUrl` to
`DocReceiverElement.Ink` using the new `MapDataUrlToRequiredBytes`
extension method. Added `MapDataUrlToRequiredBytes` to handle
base64-encoded data URLs, converting them to byte arrays.

Introduced a `using System;` directive in `AutoMapperAuditingExtensions.cs`
to support `DateTime`. Retained `MapChangedWhen` functionality while
extending mapping capabilities for handling base64 data URLs.
2026-06-10 12:36:35 +02:00
a49dd0ff81 Replace SignatureDto with new Signature record
Replaced the `SignatureDto` record with a new `Signature` record to provide a more robust representation of signature data. Updated `MappingProfile` to map `Signature` to `DocReceiverElement` and removed the old `SignatureDto` mapping. Updated `SigningCommand` to use `IEnumerable<Signature>` instead of `IEnumerable<SignatureDto>`. Removed the old `MappingProfile` class and adjusted namespaces and `using` directives accordingly. These changes improve maintainability and streamline signature handling.
2026-06-10 11:38:02 +02:00
e420e8d47a Update routes to include '/example' prefix in .razor files
Updated the `@page` directives in `DocumentViewer.razor`,
`ReportDesigner.razor`, `ReportViewer.razor`, and `TestViewer.razor`
to prepend the `/example` prefix to their respective routes.

This change modifies the URL paths for accessing these components
to better organize or namespace the routes under the `/example`
prefix. No functional or structural changes were made to the
components themselves.
2026-06-10 11:24:37 +02:00
d7f86adffe Certainly! Please provide the list of code changes so I can help craft a concise and comprehensive commit message for you. 2026-06-10 11:24:12 +02:00
cfb5c15fda Add ReadDocReceiverElementQuery and handler
Introduced `ReadDocReceiverElementQuery` for retrieving document receiver elements and its corresponding handler. Added necessary `using` directives for dependencies like `AutoMapper`, `MediatR`, and `Microsoft.EntityFrameworkCore`.

The handler dynamically filters `DocReceiverElement` data based on optional query parameters (e.g., `Envelope.Id`, `Envelope.Uuid`, `Receiver.Id`, `Receiver.Signature`) using LINQ. Data is fetched asynchronously and mapped to DTOs using `AutoMapper`.
2026-06-10 10:31:56 +02:00
332 changed files with 21780 additions and 6036 deletions

5
.gitignore vendored
View File

@@ -365,3 +365,8 @@ FodyWeavers.xsd
/EnvelopeGenerator.GeneratorAPI/ClientApp/envelope-generator-ui/.vscode /EnvelopeGenerator.GeneratorAPI/ClientApp/envelope-generator-ui/.vscode
/EnvelopeGenerator.Tests.Application/Services/BugFixTests.cs /EnvelopeGenerator.Tests.Application/Services/BugFixTests.cs
/EnvelopeGenerator.Tests.Application/annotations.json /EnvelopeGenerator.Tests.Application/annotations.json
/EnvelopeGenerator.Server/EnvelopeGenerator.Server/TekH - SoftHSM Test.md
/EnvelopeGenerator.Server/EnvelopeGenerator.Server/tekh_softHSM_test.md
/EnvelopeGenerator.Server/EnvelopeGenerator.Server/publish-output
/EnvelopeGenerator.Server/EnvelopeGenerator.Server/tekh_softHSM_test.md
/ignore/EnvelopeGenerator.Form

331
AGENTS.md Normal file
View File

@@ -0,0 +1,331 @@
# EnvelopeGenerator - Agent Guide
## Must Read First
- **`COPILOT_CONTEXT.md`** - Architecture, coordinate systems, migration status
- **`FORM_APPLICATION_CONTEXT.md`** - Legacy VB.NET features to migrate
---
## Documentation Catalog
This section explains every markdown file in the repo: what it contains, why it exists, and when you need it. Read this before deciding which file to open.
### Core Architecture Docs (Active — Always Relevant)
#### `AGENTS.md` ← you are here
**What:** Authoritative agent/copilot onboarding guide. Covers project structure, route table, render mode rules, coordinate system, API gaps, and common mistakes.
**Why it exists:** Single source of truth for agents so they don't re-derive architecture from code on every session.
**When you need it:** Always. Read first on every session.
**Maintenance:** Update whenever project structure, routes, render modes, or naming conventions change.
#### `COPILOT_CONTEXT.md`
**What:** Deep-dive architecture reference. Covers the full Blazor Auto hybrid setup, authentication flows (JWT + cookies, per-envelope receiver tokens), service registration patterns, coordinate system proof, and an explicit "prefer Server/Server.Client over old WebUI/ReceiverUI" rule.
**Why it exists:** Prevents agents from re-deriving or misinterpreting the hybrid SSR+WASM architecture. Also documents coordinate system evidence to prevent regressions (inches vs. points vs. pixels).
**When you need it:**
- Implementing any new page or service
- Debugging auth/cookie issues
- Any coordinate conversion question
- Unsure which project a new file should go in
**Maintenance:** Update when render mode assignments change, new auth schemes are added, or the coordinate system is touched.
#### `FORM_APPLICATION_CONTEXT.md`
**What:** Comprehensive reverse-engineering of the legacy VB.NET WinForms app (`EnvelopeGenerator.Form`). Documents every form (frmMain, frmEnvelopeEditor, frmFieldEditor, etc.), their UI layout, toolbar actions, data models, coordinate system, and a feature-by-feature migration mapping table to `EnvelopeGenerator.Server`.
**Why it exists:** The WinForms app is the functional specification for the sender-side web UI. It documents the full intended workflow that is being migrated to Blazor — especially the envelope creation, signature field placement, and dashboard features that are not yet implemented.
**When you need it:**
- Implementing any sender-side feature (`/sender`, `/sender/editor`, `/sender/envelope/{id}`)
- Understanding what a specific toolbar button or form should do
- Designing API endpoints for sender workflow
- Checking coordinate system behavior (INCHES proof is here)
**Maintenance:** Update the mapping table as features are implemented (change `❌ Not implemented` → `✅ Exists`). Do not rename `ReceiverUI` back — it was the old project name; current target is `EnvelopeGenerator.Server`.
#### `RECEIVER_PDF_VIEWER_CONTEXT.md`
**What:** Detailed technical reference for the receiver-side PDF viewer (`EnvelopeReceiverPage.razor`). Documents the PDF.js integration, signature button rendering, JS interop API (`window.pdfViewer`), signature capture flow, HiDPI/zoom/thumbnail config, and the full component lifecycle.
**Why it exists:** The receiver PDF viewer is the most complex component in the codebase. This doc prevents agents from misunderstanding the JS↔Blazor boundary, re-implementing already-solved problems (HiDPI, zoom, signature caching), or breaking the signing flow.
**When you need it:**
- Working on `EnvelopeReceiverPage.razor` or `pdf-viewer.js`
- Adding new signature field rendering logic
- Debugging signature rendering, zoom, or thumbnail issues
- Implementing sender-side PDF.js overlay (signature field placement) — the receiver side is the reference implementation
**Maintenance:** Update when `pdf-viewer.js` API changes, new JS interop functions are added, or the signing flow is modified.
---
### Deployment Docs
#### `EnvelopeGenerator.Server/EnvelopeGenerator.Server/README.md`
**What:** IIS publish and deployment guide for `EnvelopeGenerator.Server`. Covers why self-contained publish is required (WASM assembly version strictness), the exact `dotnet publish` command, IIS Application Pool settings (`No Managed Code`), directory structure after publish, and common deployment errors.
**Why it exists:** Blazor Auto (Server + WASM hybrid) has non-obvious deployment requirements that differ from a standard ASP.NET Core app. Forgetting self-contained publish or using the wrong app pool will break WASM loading in production.
**When you need it:**
- Deploying to IIS for the first time
- Troubleshooting 500 errors on production after deploy
- Setting up a new deployment environment
**Maintenance:** Update if the publish target, IIS config, or runtime version changes.
---
## Active Architecture (Post-Migration)
**Frontend:** Blazor Auto (Server+WASM hybrid)
- **EnvelopeGenerator.Server** (Server): `@rendermode InteractiveServer` - PDF viewers requiring DevExpress backend
- **EnvelopeGenerator.Server.Client** (WASM): `@rendermode InteractiveWebAssembly` - Login, dashboards, business logic
**Backend + API Host:** `EnvelopeGenerator.Server` (ASP.NET Core 8.0, merged Blazor + Web API host)
**Proxy:** YARP in `EnvelopeGenerator.Server` is used for external integrations (for example AuthHub forwarding), not for routing app APIs to `EnvelopeGenerator.API`.
### Deprecated Projects - DO NOT USE
- `EnvelopeGenerator.ReceiverUI` - Pure WASM (migrated to Server)
- `EnvelopeGenerator.Web` - Razor Pages (replaced by Server)
- **VB.NET projects** (`Form`, `Service`, `BBTests`) - Legacy, read-only for reference
## Development Commands
### Run Active Host
```powershell
# Terminal - Blazor + Web API host
cd EnvelopeGenerator.Server\EnvelopeGenerator.Server
dotnet run
```
**Critical:** `EnvelopeGenerator.Server` is the active runtime host for both UI and API controllers.
### Build
```powershell
dotnet build EnvelopeGenerator.sln
```
## Project Boundaries
```
EnvelopeGenerator.Domain/ # Entities (Envelope, Receiver, Document, etc.)
EnvelopeGenerator.Application/ # MediatR CQRS (Commands, Queries, Handlers)
EnvelopeGenerator.Infrastructure/ # EF Core, SQL executors, repositories
EnvelopeGenerator.Server/
EnvelopeGenerator.Server/ # Server-side Blazor + API controllers
├─ Components/Pages/ # @rendermode InteractiveServer
├─ Controllers/ # Active Web API endpoints
EnvelopeGenerator.Server.Client/ # Client-side WASM components
├─ Pages/ # @rendermode InteractiveWebAssembly
├─ Services/ # HTTP API clients
├─ Models/ # DTOs
```
## Route Structure (Critical)
| Route | File Location | Render Mode | Purpose |
|-------|--------------|-------------|---------|
| `/` | `Server.Client/Pages/Index.razor` | WASM | Landing page |
| `/sender/login` | `Server.Client/Pages/LoginSenderPage.razor` | WASM | Sender auth |
| `/sender` | `Server/Components/Pages/EnvelopeSenderPage.razor` | **Server** | Sender dashboard |
| `/sender/editor` | `Server/Components/Pages/EnvelopeSenderEditorPage.razor` | **Server** | Envelope editor + PDF viewer |
| `/envelope/login/{key}` | `Server.Client/Pages/LoginReceiverPage.razor` | WASM | Receiver auth |
| `/envelope/{key}` | `Server/Components/Pages/EnvelopeReceiverPage.razor` | **Server** | PDF viewer + signing |
**Rule:** PDF viewers MUST use `@rendermode InteractiveServer` (DevExpress backend requirement). Everything else uses WASM.
## Coordinate System (CRITICAL)
**Database stores INCHES** (GdPicture14 native). Origin: top-left, Y-axis down.
### Conversions
```csharp
// Database (INCHES) → PDF Points
float points = inches * 72;
// Database (INCHES) → DevExpress DX
float dx = inches * 100;
// PDF.js Pixels → Database (INCHES)
float inches = (pixelX / canvasWidth) * pageWidthInches;
```
**A4 Page:** 8.27" wide × 11.69" tall = 595pt × 842pt
**Signature Field Size:** 1.77" × 1.96" (FIXED, do not change)
**Evidence:** See `COPILOT_CONTEXT.md` lines 158-185, `EnvelopeGenerator.Form/frmFieldEditor.vb`
## API Architecture Quirks
### Monolithic Endpoint (Avoid for UI)
`POST /api/EnvelopeReceiver` - Creates envelope+document+receivers+fields atomically.
- **Use case:** External API consumers
- **Not suitable for:** Step-by-step UI workflow (no draft support, no partial updates)
### Sender Workflow Endpoint
```
POST /api/Envelope # Create/update/save/send envelope workflow payload
DELETE /api/Envelope/{id} # Delete with reason
POST /api/Envelope/{id}/document # Upload PDF
POST /api/Envelope/{id}/receivers # Add receiver
POST /api/Envelope/{id}/signature-fields # Place signature field
```
See `FORM_APPLICATION_CONTEXT.md` for detailed workflow requirements.
## Status Color Coding
Form app uses DevExpress `CustomDrawCell`. EnvelopeGenerator.Server needs CSS:
```css
.envelope-row.status-partly-signed { background-color: #81C784; } /* GREEN_300 */
.envelope-row.status-queued,
.envelope-row.status-sent { background-color: #FFB74D; } /* ORANGE_300 */
.envelope-row.status-completed { background-color: #81C784; }
.envelope-row.status-deleted,
.envelope-row.status-rejected { background-color: #E57373; } /* RED_300 */
```
## Configuration
### YARP Proxy (`EnvelopeGenerator.Server/yarp.json`)
Routes `/api/*`, `/swagger/*`, `/openapi/*`, `/scalar/*` → `https://localhost:8088`
### PDF.js Settings (`EnvelopeGenerator.Server/wwwroot/appsettings.json`)
```json
{
"PdfViewerOptions": {
"ThumbnailBaseScale": 0.75,
"ThumbnailEnableHiDPI": true,
"MainCanvasEnableHiDPI": true,
"ZoomStepPercentage": 5
}
}
```
### Server Host Config (`EnvelopeGenerator.Server/EnvelopeGenerator.Server/appsettings.json`)
- `ConnectionStrings:Default` - SQL Server DB
- `AllowedOrigins` - CORS (includes `http://localhost:5131`, `http://localhost:7192`)
- `Cache:SignatureCacheExpiration` - Signature persistence timeout
- `PSPDFKitLicenseKey` - **DEPRECATED** (use PDF.js instead)
## Migration Status
### Complete ✅
- Receiver login/authentication
- PDF viewing with PDF.js (HiDPI, zoom, thumbnails)
- Signature capture (draw/type/image)
- Signature caching (Redis/SQL)
- Sender login
### Missing (High Priority) ❌
- Sender dashboard (`/sender`) - Empty stub
- Envelope editor (`/sender/envelope/{id}`)
- Signature field placement tool (PDF.js + draggable overlays)
- Granular API endpoints (draft, receivers, fields)
- Master-detail grids for receivers/history
## Common Mistakes (DO NOT REPEAT)
| Mistake | Why Wrong |
|---------|-----------|
| Using iText7 in receiver pages | GPL license issue. Use PDF.js overlays. |
| Using PSPDFKit | Removed from architecture. Use PDF.js + DevExpress. |
| `@rendermode InteractiveWebAssembly` on PDF viewers | DevExpress DxPdfViewer requires server-side rendering. |
| Hardcoded quality in PDF.js | Use `appsettings.json` `PdfViewerOptions`. |
| Coordinates in points/pixels for DB | Database uses INCHES. Convert before save. |
| `BottomMarginBand` for signatures | Repeats on every page. Use `DetailBand`. |
## Testing
**No automated tests exist yet.**
Manual testing workflow:
1. Start EnvelopeGenerator.Server (`dotnet run` in `EnvelopeGenerator.Server\EnvelopeGenerator.Server`)
2. Navigate to `https://localhost:5131` (or check console output for port)
3. Test sender login at `/sender/login`
4. Test receiver flow at `/envelope/login/{envelopeKey}`
## Technical Debt TODOs
- Refactor `EnvelopeGenerator.Server/EnvelopeGenerator.Server/Components/Pages/EnvelopeSenderPage.razor` to reduce complexity.
- Move sender dashboard business/status logic into dedicated services and extension methods (for example effective status resolution, tab classification, receiver signed/rejected detection, and grid-layout filter sanitation).
- Keep `EnvelopeSenderPage.razor` focused on UI composition/state orchestration; avoid embedding heavy domain logic directly in the Razor component.
- Apply a modular page architecture across Server pages (starting with `EnvelopeGenerator.Server/EnvelopeGenerator.Server/Components/Pages/EnvelopeSenderPage.razor`): split large pages into smaller subcomponents/partial modules, eliminate duplicated code paths, and centralize shared UI/domain logic for maintainability.
- Perform a senior-level page performance review across sender/receiver flows before further UX changes: verify route transitions, component mount/unmount behavior, loading-state correctness, duplicate API calls, and perceived latency (first meaningful paint and return-navigation responsiveness). Treat loading logic defects as architecture/performance issues first, not styling problems.
- Audit `EnvelopeGenerator.Server/EnvelopeGenerator.Server.Client/Services/` for unnecessary request/payload remapping and remove redundant mappings where direct request DTO usage is safe; review client HTTP methods end-to-end for similar avoidable transformation layers.
- Centralize dependency injection registrations into dedicated extension methods/modules (especially server + client service registrations in `Program.cs`) to reduce startup composition sprawl and improve maintainability.
- Refactor `EnvelopeGenerator.Server/EnvelopeGenerator.Server/Controllers/PdfRenderController.cs` + matching client service contract to remove JSON/base64 `byte[]` transport for PDF rendering; prefer binary streaming (`application/octet-stream` or multipart), avoid `data:` URL bloat where possible, and validate end-to-end memory/latency behavior with large PDFs.
- Establish a DevExpress toast-based notification architecture using `DxToastProvider` + `IToastNotificationService` (tab/page-scoped provider strategy, standardized `ToastOptions` factory, and severity-to-style mapping) so sender/receiver flows show consistent, non-duplicated, actionable feedback.
- Eliminate manual property-by-property request→domain mapping drift (example: `CreateEnvelopeCommandHandler`) by introducing a standardized mapping policy (AutoMapper/profile conventions or reflection/source-generator based mapper) and enforcing it across envelope workflows; this needs to be solved in the shared company Core library level so all services get the same guardrails by default.
- P1 security workstream: remove dynamic SQL string composition/interpolation from active save/create/update paths (`string.Format`, `$"...{input}..."`, `ToSqlParam`) and migrate to parameterized queries only; treat this as a potential SQL injection vulnerability (stability + security risk), execute repo-wide audit, and add CI guardrails to block reintroduction.
- Alternative UX design (Server component): in `EnvelopeGenerator.Server/EnvelopeGenerator.Server/Components/Pages/EnvelopeSenderPage.razor`, enforce deterministic row double-click behavior as exactly one action (preview OR edit), and keep row action buttons/tooltips aligned with the same rule to eliminate ambiguous navigation.
## Database
**SQL Server** (DD_ECM)
- Connection string in `EnvelopeGenerator.Server/EnvelopeGenerator.Server/appsettings.json`
- EF Core migrations NOT used (manual SQL scripts)
- Stored procedures: `PRSIG_*` prefix
**Key Tables:**
- `TBSIG_ENVELOPE` - Envelope metadata
- `TBSIG_ENVELOPE_RECEIVER` - Receiver assignments
- `TBSIG_DOC_RECEIVER_ELEMENT` - Signature fields (X, Y in INCHES)
- `TBSIG_RECEIVER` - Receiver registry
- `TBSIG_DOCUMENT` - PDF binary data
- `TBSIG_ENVELOPE_HISTORY` - Audit trail
## DevExpress
**License:** Commercial (v25.2.3)
**Components Used:**
- `DxGrid` - Master-detail grids
- `DxPdfViewer` - Server-side PDF rendering
- `DxPopup` - Modal dialogs
- `DxToolbar` - Action bars
- `DxFormLayout` - Forms
**Theme:** Blazing Berry (default)
## JavaScript Interop
**PDF Viewer:** `wwwroot/js/pdf-viewer.js`
```javascript
window.pdfViewer = {
initialize(canvasId, pdfDataUrl, dotNetRef),
renderPage(num),
renderSignatureButtons(signatures, pageNum, dotNetRef),
applySignature(signatureId, dataUrl, fullName, position, place),
zoomIn(), zoomOut(), dispose()
}
```
**Signature Pad:** `wwwroot/js/receiver-signature.js`
```javascript
window.receiverSignature = {
initializeDrawPad(canvasId, dotNetRef),
getSignatureDataUrl(canvasId),
clearPad(canvasId)
}
```
## Multi-Envelope Support
Receivers can login to **multiple envelopes simultaneously** via per-envelope cookies:
```
AuthTokenSignFLOWReceiver.{envelopeKey}
```
Each envelope maintains independent authentication state.
## External Dependencies
**CDN:**
- PDF.js 3.11.174: `https://cdnjs.cloudflare.com/ajax/libs/pdf.js/3.11.174/pdf.min.js`
**NuGet (Server.Client):**
- `DevExpress.Blazor.*` 25.2.3
- `SkiaSharp.*` 3.119.1 (WASM rendering)
**External Services:**
- LDAP/AD authentication (optional)
- GTX Messaging (SMS 2FA)
- Email dispatcher (signFlow)
## Environment Variables
None required. All config in `appsettings.json`.
**Local dev ports:**
- App host (UI + API): `https://localhost:5131` or `http://localhost:7192`

351
COPILOT_CONTEXT.md Normal file
View File

@@ -0,0 +1,351 @@
# EnvelopeGenerator — Current Workspace Context
## Purpose
Digital document signing system for senders and receivers.
- Senders authenticate, view envelope lists, and manage envelope workflows.
- Receivers authenticate per envelope, open PDFs, create signatures, and apply them in the viewer.
- The active UI stack is `Blazor Auto` with server-side and WebAssembly render modes.
- Primary UI/PDF libraries are `DevExpress` and `PDF.js`.
---
## Active Application Structure
### Main Host
**Primary active application:** `EnvelopeGenerator.Server`
`EnvelopeGenerator.Server` is the current runtime host and contains:
- Blazor server host
- WebAssembly host integration
- API controllers
- authentication/authorization setup
- Swagger/Scalar setup
- YARP reverse proxy configuration
- DevExpress server-side services
- SQL Server distributed cache setup
### Client Project
**Client UI project:** `EnvelopeGenerator.Server.Client`
This project contains:
- WebAssembly-rendered pages
- client-side services
- client models and options
- sender and receiver login flows
### Other Projects
- `EnvelopeGenerator.Application` — MediatR/CQRS handlers and business logic
- `EnvelopeGenerator.Domain` — domain models, constants, shared abstractions
- `EnvelopeGenerator.Infrastructure` — EF Core and infrastructure services
- `EnvelopeGenerator.PdfEditor` — PDF-related backend utilities
- `EnvelopeGenerator.API` — deprecated project (no longer part of the active solution/runtime path)
### Legacy / Do Not Touch
- `EnvelopeGenerator.Service`
- `EnvelopeGenerator.Form`
- `EnvelopeGenerator.BBTests`
- `EnvelopeGenerator.CommonServices`
---
## Current Hosting Model
`EnvelopeGenerator.Server/Program.cs` currently configures:
- `AddRazorComponents()` with both interactive server and interactive WebAssembly components
- `AddControllers()` and `MapControllers()`
- JWT authentication for sender and receiver flows
- cookie authentication
- authorization policies using `AuthScheme.Sender`, `AuthScheme.Receiver`, `AuthPolicy.Sender`, `AuthPolicy.Receiver`
- `AddReverseProxy()` with `yarp.json`
- Swagger / OpenAPI / Scalar
- distributed SQL Server cache
- DevExpress Blazor and DevExpress PDF Viewer server-side services
- request localization middleware
This means the active app is a **merged UI + API host**.
---
## Reverse Proxy
**Config file:** `EnvelopeGenerator.Server/EnvelopeGenerator.Server/yarp.json`
Current YARP usage is focused on **AuthHub forwarding**, not a general `/api/* -> EnvelopeGenerator.API` proxy.
Configured routes forward:
- `POST /api/auth` -> AuthHub `/api/auth/sign-flow`
- `POST /api/Auth/envelope-receiver/{key}` -> AuthHub `/api/auth/envelope-receiver/{key}?cookie=true`
---
## Active Routes and Files
### WebAssembly Pages (`EnvelopeGenerator.Server.Client`)
| Route | File | Render Mode | Purpose |
|---|---|---|---|
| `/` | `EnvelopeGenerator.Server/EnvelopeGenerator.Server.Client/Pages/IndexPage.razor` | WebAssembly | Landing page |
| `/sender/login` | `EnvelopeGenerator.Server/EnvelopeGenerator.Server.Client/Pages/LoginSenderPage.razor` | WebAssembly | Sender login |
| `/sender` | `EnvelopeGenerator.Server/EnvelopeGenerator.Server.Client/Pages/EnvelopeSenderPage.razor` | WebAssembly (`prerender: false`) | Sender dashboard |
| `/envelope/login/{EnvelopeKey}` | `EnvelopeGenerator.Server/EnvelopeGenerator.Server.Client/Pages/LoginReceiverPage.razor` | WebAssembly | Receiver login |
### Server Pages (`EnvelopeGenerator.Server`)
| Route | File | Render Mode | Purpose |
|---|---|---|---|
| `/envelope/{EnvelopeKey}` | `EnvelopeGenerator.Server/EnvelopeGenerator.Server/Components/Pages/EnvelopeReceiverPage.razor` | InteractiveServer | Main receiver PDF viewer and signing page |
| `/envelope/DxPdfViewer` | `EnvelopeGenerator.Server/EnvelopeGenerator.Server/Components/Pages/EnvelopeReceiverPage_DxPdfViewer.razor` | InteractiveServer | DevExpress PDF Viewer test page |
| `/envelope/{EnvelopeKey}/DxReportViewer` | `EnvelopeGenerator.Server/EnvelopeGenerator.Server/Components/Pages/EnvelopeReceiverPage_DxReportViewer.razor` | InteractiveServer | DevExpress report-based PDF rendering |
| `/envelope/Embed` | `EnvelopeGenerator.Server/EnvelopeGenerator.Server/Components/Pages/EnvelopeReceiverPage_embed.razor` | InteractiveServer | Embedded browser PDF view test page |
---
## Current API Location
The active application exposes controllers from:
`EnvelopeGenerator.Server/EnvelopeGenerator.Server/Controllers`
Current controller set includes:
- `AnnotationController`
- `AuthController`
- `CacheController`
- `ConfigController`
- `DocumentController`
- `EmailTemplateController`
- `EnvelopeController`
- `EnvelopeReceiverController`
- `EnvelopeTypeController`
- `HistoryController`
- `LocalizationController`
- `ReadOnlyController`
- `ReceiverController`
- `SignatureController`
- `TfaRegistrationController`
Do not assume API behavior lives in `EnvelopeGenerator.API`; the active merged host contains controller endpoints directly.
---
## Authentication Model
### Sender
Client login page uses `EnvelopeGenerator.Server.Client/Services/AuthService.cs`.
Key sender endpoints:
- `POST /api/auth?cookie=true` — login
- `GET /api/auth/check` — current sender access check
- `POST /api/auth/logout` — logout
### Receiver
Receiver authentication is **per envelope**.
Key receiver endpoints used by client services:
- `POST /api/Auth/envelope-receiver/{envelopeKey}` — submit access code
- `GET /api/auth/check/envelope/{envelopeKey}` — check access
- `POST /api/auth/logout/envelope/{envelopeKey}` — logout receiver for one envelope
Receiver cookie resolution in server auth uses an envelope-specific cookie name derived from:
- `AuthTokenSignFLOWReceiver.{envelopeKey}` pattern
### Receiver Server-Side Authorization
`EnvelopeReceiverPage.razor` does **not** rely on its own API access-check call for page authorization.
It uses:
- `EnvelopeGenerator.Server/EnvelopeGenerator.Server/Services/EnvelopeReceiverAuthorizationService.cs`
Behavior:
- tries the current `HttpContext.User`
- if needed, reads the per-envelope receiver cookie directly
- validates the JWT with the receiver auth scheme
- verifies the token subject matches the route envelope key
---
## Receiver Page Data Loading
Main server-side page data service:
- `EnvelopeGenerator.Server/EnvelopeGenerator.Server/Services/EnvelopeReceiverPageDataService.cs`
This service loads directly via MediatR and distributed cache:
- document bytes
- receiver envelope data
- signature placeholders
- cached signature data
For signature placeholders, the service:
- reads document receiver elements
- filters them for the authenticated receiver
- converts coordinates to `UnitOfLength.Point` before UI use
---
## Receiver PDF Viewer
**Main file:** `EnvelopeGenerator.Server/EnvelopeGenerator.Server/Components/Pages/EnvelopeReceiverPage.razor`
Current receiver viewer characteristics:
- route: `/envelope/{EnvelopeKey}`
- render mode: `InteractiveServer`
- PDF rendering: `PDF.js`
- toolbar: page navigation, zoom, thumbnail toggle, signature navigation, signature reset
- signature popup: `DxPopup`
- thumbnail sidebar: resizable and stored in `localStorage`
### JS Assets
- `EnvelopeGenerator.Server/EnvelopeGenerator.Server/wwwroot/js/pdf-viewer.js`
- `EnvelopeGenerator.Server/EnvelopeGenerator.Server/wwwroot/js/receiver-signature.js`
### CSS
- `EnvelopeGenerator.Server/EnvelopeGenerator.Server/wwwroot/css/envelope-viewer.css`
### PDF.js CDN
- `https://cdnjs.cloudflare.com/ajax/libs/pdf.js/3.11.174/pdf.min.js`
- `https://cdnjs.cloudflare.com/ajax/libs/pdf.js/3.11.174/pdf_viewer.min.css`
---
## Signature Workflow
Receiver signatures are handled as a **viewer overlay workflow**.
### Current behavior
1. Server-side authorization validates receiver access.
2. The page loads document bytes, receiver data, signature placeholders, and cached signature state.
3. If no cached signature exists, the signature popup opens automatically.
4. Receiver creates signature using one of three tabs:
- draw
- text
- image
5. Required metadata:
- full name
- place
6. Optional metadata:
- position
7. Clicking a signature placeholder applies the signature as a client-side overlay in the PDF viewer.
### Important note
Although `itext` is referenced by the server project, the current receiver page signing flow is **not PDF stamping-based**. The active receiver UI uses client-side overlay behavior in the viewer.
### Signature DTO
`EnvelopeGenerator.Server.Client/Models/SignatureCaptureDto.cs`
```csharp
public sealed record SignatureCaptureDto {
public required string DataUrl { get; init; }
public required string FullName { get; init; }
public string Position { get; init; } = "";
public required string Place { get; init; }
}
```
---
## Signature Cache
### Active cache model
The current receiver page cache flow is handled directly in the server project through:
- `EnvelopeReceiverPageDataService`
- `IDistributedCache`
- SQL Server distributed cache configuration from `Program.cs`
### Cache key format
Current server-side key prefix:
- `envelope-generator.receiver-ui.signature:{receiverSignature}`
This is different from an envelope-key-only cache convention.
### Config
`EnvelopeGenerator.Server/EnvelopeGenerator.Server/Options/CacheOptions.cs`
- section name: `Cache`
- option: `SignatureCacheExpiration`
### Related controller
A cache API controller also exists in:
- `EnvelopeGenerator.Server/EnvelopeGenerator.Server/Controllers/CacheController.cs`
---
## Sender Dashboard
**Main file:** `EnvelopeGenerator.Server/EnvelopeGenerator.Server.Client/Pages/EnvelopeSenderPage.razor`
Current behavior:
- checks sender access through `AuthService.CheckSenderAccessAsync()`
- redirects to `/sender/login` when unauthorized
- loads envelope list through client `EnvelopeService`
- separates envelopes into active/completed tabs
- uses `DevExpress DxGrid`
The sender page is active, but create/edit/delete actions are still marked with TODO behavior in the UI page.
---
## Localization
Current server host localization setup in `Program.cs`:
- supported cultures: `de-DE`, `en-US`
- request localization middleware is enabled
- `QueryStringRequestCultureProvider` is added
- cookie-based localization services are registered via `AddCookieBasedLocalizer()`
Do not assume the old ReceiverUI-only `localStorage` culture approach is the current source of truth for the active host.
---
## Coordinate System
### Source data
Database signature coordinates are still based on:
- **unit:** inches
- **origin:** top-left
- **axes:** X right, Y down
### Relevant conversions
- inches -> PDF points: `x_pt = x_inches * 72`
- inches -> DevExpress DX units: `x_dx = x_inches * 100`
### Current receiver page behavior
The server page data service converts signature placeholders to **points** before sending them into the viewer workflow.
### Unit systems to keep in mind
| System | Unit | Origin | Y-axis |
|---|---|---|---|
| Database | Inches | Top-left | Down |
| PDF.js display | Pixels | Top-left | Down |
| PDF points | Points | Depends on PDF model | Depends on consumer |
| DevExpress DX | 1/100 inch style coordinates | Top-left-oriented usage in this app | Down-oriented usage |
---
## Key Services and Files
### Client services
- `EnvelopeGenerator.Server/EnvelopeGenerator.Server.Client/Services/AuthService.cs`
- `EnvelopeGenerator.Server/EnvelopeGenerator.Server.Client/Services/EnvelopeService.cs`
- `EnvelopeGenerator.Server/EnvelopeGenerator.Server.Client/Services/DocumentService.cs`
- `EnvelopeGenerator.Server/EnvelopeGenerator.Server.Client/Services/SignatureCacheService.cs`
### Server services
- `EnvelopeGenerator.Server/EnvelopeGenerator.Server/Services/EnvelopeAuthService.cs`
- `EnvelopeGenerator.Server/EnvelopeGenerator.Server/Services/IEnvelopeAuthService.cs`
- `EnvelopeGenerator.Server/EnvelopeGenerator.Server/Services/EnvelopeReceiverAuthorizationService.cs`
- `EnvelopeGenerator.Server/EnvelopeGenerator.Server/Services/EnvelopeReceiverPageDataService.cs`
### Server config and host files
- `EnvelopeGenerator.Server/EnvelopeGenerator.Server/Program.cs`
- `EnvelopeGenerator.Server/EnvelopeGenerator.Server/yarp.json`
---
## Working Rules for This Workspace
- Treat `EnvelopeGenerator.Server` as the active main application host.
- Treat `EnvelopeGenerator.Server.Client` as the active client UI project.
- Prefer current `Server` / `Server.Client` paths over old `WebUI` / `ReceiverUI` references.
- Do not use `EnvelopeGenerator.Web` or `EnvelopeGenerator.ReceiverUI` as the primary implementation target unless explicitly asked.
- Do not modify the legacy VB.NET projects unless explicitly requested.
- For receiver PDF/signature work, prefer the current `PDF.js`-based flow in `EnvelopeReceiverPage.razor`.
- For DevExpress PDF viewer issues, remember server-side services are registered in `EnvelopeGenerator.Server`.
---
**Last Updated:** 2026-06-29

File diff suppressed because it is too large Load Diff

View File

@@ -1,57 +0,0 @@
using EnvelopeGenerator.API.Extensions;
using EnvelopeGenerator.Application.Common.Dto;
using EnvelopeGenerator.Application.Common.Extensions;
using EnvelopeGenerator.Application.Documents.Queries;
using EnvelopeGenerator.Domain.Constants;
using MediatR;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
namespace EnvelopeGenerator.API.Controllers;
/// <summary>
///
/// </summary>
[Authorize(Policy = AuthPolicy.Receiver)]
[ApiController]
[Route("api/[controller]")]
public class SignatureController : ControllerBase
{
private readonly IMediator _mediator;
/// <summary>
/// Initializes a new instance of <see cref="SignatureController"/>.
/// </summary>
public SignatureController(IMediator mediator)
{
_mediator = mediator;
}
//TODO: update to use signature query
/// <summary>
///
/// </summary>
/// <param name="envelopeKey"></param>
/// <param name="cancel"></param>
/// <returns></returns>
[Authorize(Policy = AuthPolicy.Receiver)]
[HttpGet("{envelopeKey}")]
public async Task<IActionResult> Get(string envelopeKey, CancellationToken cancel)
{
int envelopeId = User.EnvelopeId();
int receiverId = User.ReceiverId();
var doc = await _mediator.Send(new ReadDocumentQuery() { EnvelopeId = envelopeId }, cancel);
if (doc.Elements is not IEnumerable<DocReceiverElementDto> docSignatures)
return NotFound("Document is empty.");
var rcvSignatures = docSignatures.Where(s => s.ReceiverId == receiverId).ToList();
if (rcvSignatures is null)
return NotFound("No signatures found for the current receiver.");
else
return Ok(rcvSignatures);
}
}

View File

@@ -1,83 +0,0 @@
<Project Sdk="Microsoft.NET.Sdk.Web">
<PropertyGroup>
<TargetFrameworks>net8.0</TargetFrameworks>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
<GenerateDocumentationFile>true</GenerateDocumentationFile>
<PackageId>EnvelopeGenerator.GeneratorAPI</PackageId>
<Title></Title>
<Authors>Digital Data GmbH</Authors>
<Company>Digital Data GmbH</Company>
<Product>EnvelopeGenerator.GeneratorAPI</Product>
<Version>1.4.0</Version>
<FileVersion>1.4.0</FileVersion>
<AssemblyVersion>1.4.0</AssemblyVersion>
<PackageOutputPath>Copyright © 2025 Digital Data GmbH. All rights reserved.</PackageOutputPath>
<DocumentationFile>bin\$(Configuration)\$(TargetFramework)\$(AssemblyName).xml</DocumentationFile>
</PropertyGroup>
<ItemGroup>
<Compile Remove="ClientApp\**" />
<Content Remove="ClientApp\**" />
<EmbeddedResource Remove="ClientApp\**" />
<None Remove="ClientApp\**" />
</ItemGroup>
<ItemGroup>
<None Include="yarp.json" CopyToOutputDirectory="PreserveNewest" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="AspNetCore.Scalar" Version="1.1.8" />
<PackageReference Include="DigitalData.Auth.Claims" Version="1.0.3" />
<PackageReference Include="DigitalData.Auth.Client" Version="1.3.7" />
<PackageReference Include="DigitalData.Core.API" Version="2.2.1" />
<PackageReference Include="HtmlSanitizer" Version="9.0.892" />
<PackageReference Include="Microsoft.Extensions.Caching.SqlServer" Version="8.0.11" Condition="'$(TargetFramework)' == 'net8.0'" />
<PackageReference Include="itext" Version="8.0.5" />
<PackageReference Include="itext.bouncy-castle-adapter" Version="8.0.5" />
<PackageReference Include="Microsoft.AspNetCore.OpenApi" Version="9.0.4" Condition="'$(TargetFramework)' == 'net9.0'" />
<PackageReference Include="Microsoft.AspNetCore.OpenApi" Version="8.0.11" Condition="'$(TargetFramework)' == 'net8.0'" />
<PackageReference Include="Microsoft.EntityFrameworkCore.SqlServer" Version="9.0.6" Condition="'$(TargetFramework)' == 'net9.0'" />
<PackageReference Include="Microsoft.EntityFrameworkCore.SqlServer" Version="8.0.17" Condition="'$(TargetFramework)' == 'net8.0'" />
<PackageReference Include="Microsoft.Identity.Client" Version="4.82.1" />
<PackageReference Include="NLog" Version="5.2.5" />
<PackageReference Include="NLog.Web.AspNetCore" Version="5.3.0" />
<PackageReference Include="Scalar.AspNetCore" Version="2.2.1" />
<PackageReference Include="SixLabors.ImageSharp" Version="3.1.12" />
<PackageReference Include="Swashbuckle.AspNetCore" Version="8.1.1" />
<PackageReference Include="DigitalData.EmailProfilerDispatcher.Abstraction" Version="3.2.0" />
<PackageReference Include="Yarp.ReverseProxy" Version="2.1.0" />
</ItemGroup>
<ItemGroup Condition="'$(TargetFramework)' == 'net7.0'">
<PackageReference Include="System.DirectoryServices" Version="7.0.1" />
<PackageReference Include="System.DirectoryServices.AccountManagement" Version="7.0.1" />
<PackageReference Include="System.DirectoryServices.Protocols" Version="7.0.1" />
</ItemGroup>
<ItemGroup Condition="'$(TargetFramework)' == 'net8.0'">
<PackageReference Include="System.DirectoryServices" Version="8.0.0" />
<PackageReference Include="System.DirectoryServices.AccountManagement" Version="8.0.1" />
<PackageReference Include="System.DirectoryServices.Protocols" Version="8.0.1" />
</ItemGroup>
<ItemGroup Condition="'$(TargetFramework)' == 'net9.0'">
<PackageReference Include="System.DirectoryServices" Version="9.0.4" />
<PackageReference Include="System.DirectoryServices.AccountManagement" Version="9.0.4" />
<PackageReference Include="System.DirectoryServices.Protocols" Version="9.0.4" />
</ItemGroup>
<ItemGroup>
<Folder Include="wwwroot\" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\EnvelopeGenerator.Application\EnvelopeGenerator.Application.csproj" />
<ProjectReference Include="..\EnvelopeGenerator.Domain\EnvelopeGenerator.Domain.csproj" />
<ProjectReference Include="..\EnvelopeGenerator.Infrastructure\EnvelopeGenerator.Infrastructure.csproj" />
<ProjectReference Include="..\EnvelopeGenerator.PdfEditor\EnvelopeGenerator.PdfEditor.csproj" />
</ItemGroup>
</Project>

View File

@@ -1,12 +0,0 @@
namespace EnvelopeGenerator.API.Models;
/// <summary>
///
/// </summary>
public class ConnectionString
{
/// <summary>
///
/// </summary>
public string Value { get; set; } = string.Empty;
}

View File

@@ -1,6 +0,0 @@
namespace EnvelopeGenerator.API.Models;
public class CustomImages : Dictionary<string, Image>
{
public new Image this[string key] => TryGetValue(key, out var img) && img is not null ? img : new();
}

View File

@@ -1,176 +0,0 @@
{
"ReverseProxy": {
"Routes": {
"receiver-ui-receiver": {
"ClusterId": "receiver-ui",
"Order": 100,
"Match": {
"Path": "/receiver/{**catch-all}",
"Methods": [ "GET", "HEAD" ]
}
},
"receiver-ui-login": {
"ClusterId": "receiver-ui",
"Order": 100,
"Match": {
"Path": "/login/{**catch-all}",
"Methods": [ "GET", "HEAD" ]
}
},
"receiver-ui-sender": {
"ClusterId": "receiver-ui",
"Order": 100,
"Match": {
"Path": "/sender/{**catch-all}",
"Methods": [ "GET", "HEAD" ]
}
},
"receiver-ui-envelope": {
"ClusterId": "receiver-ui",
"Order": 100,
"Match": {
"Path": "/envelope/{EnvelopeKey}",
"Methods": [ "GET", "HEAD" ]
},
"Transforms": [
{ "PathSet": "/index.html" }
]
},
"receiver-ui-blazor-framework": {
"ClusterId": "receiver-ui",
"Order": 50,
"Match": {
"Path": "/_framework/{**catch-all}",
"Methods": [ "GET", "HEAD" ]
}
},
"receiver-ui-blazor-content": {
"ClusterId": "receiver-ui",
"Order": 50,
"Match": {
"Path": "/_content/{**catch-all}",
"Methods": [ "GET", "HEAD" ]
}
},
"receiver-ui-static-css": {
"ClusterId": "receiver-ui",
"Order": 200,
"Match": {
"Path": "/css/{**catch-all}",
"Methods": [ "GET", "HEAD" ]
},
"Transforms": [
{
"ResponseHeader": "Cache-Control",
"Set": "no-cache, no-store, must-revalidate",
"When": "Always"
}
]
},
"receiver-ui-static-js": {
"ClusterId": "receiver-ui",
"Order": 200,
"Match": {
"Path": "/js/{**catch-all}",
"Methods": [ "GET", "HEAD" ]
},
"Transforms": [
{
"ResponseHeader": "Cache-Control",
"Set": "no-cache, no-store, must-revalidate",
"When": "Always"
}
]
},
"receiver-ui-fake-data": {
"ClusterId": "receiver-ui",
"Order": 200,
"Match": {
"Path": "/fake-data/{**catch-all}",
"Methods": [ "GET", "HEAD" ]
}
},
"receiver-ui-appsettings": {
"ClusterId": "receiver-ui",
"Order": 50,
"Match": {
"Path": "/appsettings.json",
"Methods": [ "GET", "HEAD" ]
}
},
"receiver-ui-appsettings-dev": {
"ClusterId": "receiver-ui",
"Order": 50,
"Match": {
"Path": "/appsettings.Development.json",
"Methods": [ "GET", "HEAD" ]
}
},
"receiver-ui-styles": {
"ClusterId": "receiver-ui",
"Order": 50,
"Match": {
"Path": "/EnvelopeGenerator.ReceiverUI.styles.css",
"Methods": [ "GET", "HEAD" ]
}
},
"receiver-ui-fonts": {
"ClusterId": "receiver-ui",
"Order": 200,
"Match": {
"Path": "/fonts/{**catch-all}",
"Methods": [ "GET", "HEAD" ]
}
},
"receiver-ui-images": {
"ClusterId": "receiver-ui",
"Order": 200,
"Match": {
"Path": "/images/{**catch-all}",
"Methods": [ "GET", "HEAD" ]
}
},
"auth-login": {
"ClusterId": "auth-hub",
"Match": {
"Path": "/api/auth",
"Methods": [ "POST" ]
},
"Transforms": [
{ "PathSet": "/api/auth/sign-flow" }
]
},
"auth-envelope-receiver-login": {
"ClusterId": "auth-hub",
"Match": {
"Path": "/api/Auth/envelope-receiver/{key}",
"Methods": [ "POST" ]
},
"Transforms": [
{ "PathPattern": "/api/auth/envelope-receiver/{key}" },
{
"QueryValueParameter": "cookie",
"Set": "true"
}
]
}
},
"Clusters": {
"receiver-ui": {
"Destinations": {
"primary": {
"Address": "https://localhost:52936"
}
}
},
"auth-hub": {
"Destinations": {
"primary": {
"Address": "http://172.24.12.39:9090"
}
}
}
}
}
}

View File

@@ -0,0 +1,40 @@
using DigitalData.Core.Exceptions;
using FluentValidation;
using MediatR;
namespace EnvelopeGenerator.Application.Common.Behaviors;
public class ValidationBehavior<TRequest, TResponse> : IPipelineBehavior<TRequest, TResponse>
where TRequest : notnull
{
private readonly IEnumerable<IValidator<TRequest>> _validators;
public ValidationBehavior(IEnumerable<IValidator<TRequest>> validators)
{
_validators = validators;
}
public async Task<TResponse> Handle(
TRequest request,
RequestHandlerDelegate<TResponse> next,
CancellationToken cancellationToken)
{
if (_validators.Any())
{
var context = new ValidationContext<TRequest>(request);
var failures = _validators
.Select(v => v.Validate(context))
.SelectMany(r => r.Errors)
.Where(f => f is not null)
.ToList();
if (failures.Count > 0)
{
var message = string.Join("; ", failures.Select(f => f.ErrorMessage).Distinct());
throw new BadRequestException(message);
}
}
return await next();
}
}

View File

@@ -1,11 +1,8 @@
using Microsoft.AspNetCore.Mvc; namespace EnvelopeGenerator.Application.Common.Dto;
namespace EnvelopeGenerator.Application.Common.Dto;
/// <summary> /// <summary>
/// Data Transfer Object representing configuration settings. /// Data Transfer Object representing configuration settings.
/// </summary> /// </summary>
[ApiExplorerSettings(IgnoreApi = true)]
public class ConfigDto public class ConfigDto
{ {
/// <summary> /// <summary>

View File

@@ -1,14 +1,11 @@
using EnvelopeGenerator.Domain.Constants; using EnvelopeGenerator.Domain.Constants;
using EnvelopeGenerator.Domain.Interfaces; using EnvelopeGenerator.Domain.Interfaces;
using Microsoft.AspNetCore.Mvc;
using System.ComponentModel.DataAnnotations.Schema;
namespace EnvelopeGenerator.Application.Common.Dto; namespace EnvelopeGenerator.Application.Common.Dto;
/// <summary> /// <summary>
/// Data Transfer Object representing a positioned element assigned to a document receiver. /// Data Transfer Object representing a positioned element assigned to a document receiver.
/// </summary> /// </summary>
[ApiExplorerSettings(IgnoreApi = true)]
public class DocReceiverElementDto : IDocReceiverElement public class DocReceiverElementDto : IDocReceiverElement
{ {
/// <summary> /// <summary>

View File

@@ -0,0 +1,27 @@
namespace EnvelopeGenerator.Application.Common.Dto;
/// <summary>
/// Data Transfer Object representing document metadata and elements without binary payload.
/// </summary>
public record DocumentDetailsDto
{
/// <summary>
/// Gets or sets the unique identifier of the document.
/// </summary>
public int Id { get; init; }
/// <summary>
/// Gets or sets the envelope ID to which the document belongs.
/// </summary>
public int EnvelopeId { get; init; }
/// <summary>
/// Gets or sets the date and time when the document was added.
/// </summary>
public DateTime AddedWhen { get; init; }
/// <summary>
/// Gets or sets the collection of elements associated with the document for receiver interactions, if any.
/// </summary>
public IEnumerable<DocReceiverElementDto>? Elements { get; init; }
}

View File

@@ -1,35 +1,27 @@
using Microsoft.AspNetCore.Mvc; namespace EnvelopeGenerator.Application.Common.Dto;
namespace EnvelopeGenerator.Application.Common.Dto;
/// <summary> /// <summary>
/// Data Transfer Object representing a document within an envelope, including optional binary data and form elements. /// Data Transfer Object representing a document within an envelope, including optional binary data and form elements.
/// </summary> /// </summary>
[ApiExplorerSettings(IgnoreApi = true)] public record DocumentDto : DocumentDetailsDto
public class DocumentDto
{ {
/// <summary> /// <summary>
/// Gets or sets the unique identifier of the document. ///
/// </summary> /// </summary>
public int Id { get; set; } public DocumentDto() { }
/// <summary> /// <summary>
/// Gets or sets the envelope ID to which the document belongs. ///
/// </summary> /// </summary>
public int EnvelopeId { get; set; } /// <param name="byteData"></param>
/// <param name="details"></param>
/// <summary> public DocumentDto(byte[] byteData, DocumentDetailsDto details) : base(details)
/// Gets or sets the date and time when the document was added. {
/// </summary> ByteData = byteData;
public DateTime AddedWhen { get; set; } }
/// <summary> /// <summary>
/// Gets or sets the binary data of the document, if available. /// Gets or sets the binary data of the document, if available.
/// </summary> /// </summary>
public byte[]? ByteData { get; set; } public byte[]? ByteData { get; init; }
}
/// <summary>
/// Gets or sets the collection of elements associated with the document for receiver interactions, if any.
/// </summary>
public IEnumerable<DocReceiverElementDto>? Elements { get; set; }
}

View File

@@ -1,12 +1,10 @@
using EnvelopeGenerator.Domain.Constants; using EnvelopeGenerator.Domain.Constants;
using Microsoft.AspNetCore.Mvc;
namespace EnvelopeGenerator.Application.Common.Dto; namespace EnvelopeGenerator.Application.Common.Dto;
/// <summary> /// <summary>
/// Data Transfer Object representing the status of a document for a specific receiver. /// Data Transfer Object representing the status of a document for a specific receiver.
/// </summary> /// </summary>
[ApiExplorerSettings(IgnoreApi = true)]
public class DocumentStatusDto public class DocumentStatusDto
{ {
/// <summary> /// <summary>

View File

@@ -1,16 +1,15 @@
using DigitalData.EmailProfilerDispatcher.Abstraction.Attributes; using DigitalData.EmailProfilerDispatcher.Abstraction.Attributes;
using DigitalData.UserManager.Application.DTOs.User; using DigitalData.UserManager.Application.DTOs.User;
using EnvelopeGenerator.Application.Common.Dto.EnvelopeReceiver;
using EnvelopeGenerator.Application.Common.Dto.History;
using EnvelopeGenerator.Domain.Constants; using EnvelopeGenerator.Domain.Constants;
using EnvelopeGenerator.Domain.Entities; using EnvelopeGenerator.Domain.Entities;
using EnvelopeGenerator.Domain.Interfaces; using EnvelopeGenerator.Domain.Interfaces;
using Microsoft.AspNetCore.Mvc;
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>
@@ -87,6 +86,41 @@ public record EnvelopeDto : IEnvelope
/// </summary> /// </summary>
public int? CertificationType { get; set; } public int? CertificationType { get; set; }
/// <summary>
///
/// </summary>
public int? FinalEmailToCreator { get; set; }
/// <summary>
///
/// </summary>
public int? FinalEmailToReceivers { get; set; }
/// <summary>
///
/// </summary>
public bool? SendReminderEmails { get; set; }
/// <summary>
///
/// </summary>
public int? FirstReminderDays { get; set; }
/// <summary>
///
/// </summary>
public int? ReminderIntervalDays { get; set; }
/// <summary>
///
/// </summary>
public int? ExpiresWhenDays { get; set; }
/// <summary>
///
/// </summary>
public int? ExpiresWarningWhenDays { get; set; }
/// <summary> /// <summary>
/// ///
/// </summary> /// </summary>
@@ -126,4 +160,14 @@ public record EnvelopeDto : IEnvelope
/// ///
/// </summary> /// </summary>
public IEnumerable<DocumentDto>? Documents { get; set; } public IEnumerable<DocumentDto>? Documents { get; set; }
}
/// <summary>
///
/// </summary>
public IEnumerable<EnvelopeReceiverDto>? EnvelopeReceivers { get; set; }
/// <summary>
/// Envelope history entries tracking actions like DocumentSigned, EnvelopeOpened, etc.
/// </summary>
public IEnumerable<HistoryDto>? Histories { get; set; }
}

View File

@@ -1,13 +1,12 @@
using DigitalData.EmailProfilerDispatcher.Abstraction.Attributes; using DigitalData.EmailProfilerDispatcher.Abstraction.Attributes;
using EnvelopeGenerator.Application.Common.Dto.Receiver; using EnvelopeGenerator.Application.Common.Dto.Receiver;
using Microsoft.AspNetCore.Mvc; using EnvelopeGenerator.Domain.Constants;
namespace EnvelopeGenerator.Application.Common.Dto.EnvelopeReceiver; namespace EnvelopeGenerator.Application.Common.Dto.EnvelopeReceiver;
/// <summary> /// <summary>
/// ///
/// </summary> /// </summary>
[ApiExplorerSettings(IgnoreApi = true)]
public record EnvelopeReceiverDto public record EnvelopeReceiverDto
{ {
/// <summary> /// <summary>
@@ -61,6 +60,11 @@ public record EnvelopeReceiverDto
/// </summary> /// </summary>
public string? PrivateMessage { get; init; } public string? PrivateMessage { get; init; }
/// <summary>
///
/// </summary>
public string? AccessCode { get; init; }
/// <summary> /// <summary>
/// ///
/// </summary> /// </summary>
@@ -75,4 +79,13 @@ public record EnvelopeReceiverDto
/// ///
/// </summary> /// </summary>
public bool HasPhoneNumber { get; init; } public bool HasPhoneNumber { get; init; }
}
/// <summary>
/// Indicates whether this receiver has signed the envelope.
/// Checks if there is a DocumentSigned history entry for this receiver in the envelope's history.
/// </summary>
public bool Signed => Envelope?.Histories?.Any(h =>
h.Receiver?.Id == ReceiverId &&
h.Status == EnvelopeStatus.DocumentSigned
) ?? false;
}

View File

@@ -1,11 +1,8 @@
using Microsoft.AspNetCore.Mvc; namespace EnvelopeGenerator.Application.Common.Dto.EnvelopeReceiver;
namespace EnvelopeGenerator.Application.Common.Dto.EnvelopeReceiver;
/// <summary> /// <summary>
/// ///
/// </summary> /// </summary>
[ApiExplorerSettings(IgnoreApi = true)]
public record EnvelopeReceiverSecretDto : EnvelopeReceiverDto public record EnvelopeReceiverSecretDto : EnvelopeReceiverDto
{ {
/// <summary> /// <summary>

View File

@@ -1,5 +1,4 @@
using Microsoft.AspNetCore.Mvc; using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations;
using System.Text.Json.Serialization; using System.Text.Json.Serialization;
namespace EnvelopeGenerator.Application.Common.Dto.EnvelopeReceiverReadOnly; namespace EnvelopeGenerator.Application.Common.Dto.EnvelopeReceiverReadOnly;
@@ -8,7 +7,6 @@ namespace EnvelopeGenerator.Application.Common.Dto.EnvelopeReceiverReadOnly;
/// ///
/// </summary> /// </summary>
/// <param name="DateValid"></param> /// <param name="DateValid"></param>
[ApiExplorerSettings(IgnoreApi = true)]
public record EnvelopeReceiverReadOnlyCreateDto( public record EnvelopeReceiverReadOnlyCreateDto(
DateTime DateValid) DateTime DateValid)
{ {

View File

@@ -1,6 +1,4 @@
using EnvelopeGenerator.Application.Common.Dto; using EnvelopeGenerator.Application.Common.Dto.Receiver;
using EnvelopeGenerator.Application.Common.Dto.Receiver;
using Microsoft.AspNetCore.Mvc;
namespace EnvelopeGenerator.Application.Common.Dto.EnvelopeReceiverReadOnly; namespace EnvelopeGenerator.Application.Common.Dto.EnvelopeReceiverReadOnly;
@@ -8,7 +6,6 @@ namespace EnvelopeGenerator.Application.Common.Dto.EnvelopeReceiverReadOnly;
/// Represents a read-only Data Transfer Object (DTO) for an envelope receiver. /// Represents a read-only Data Transfer Object (DTO) for an envelope receiver.
/// Contains information about the receiver, associated envelope, and audit details. /// Contains information about the receiver, associated envelope, and audit details.
/// </summary> /// </summary>
[ApiExplorerSettings(IgnoreApi = true)]
public class EnvelopeReceiverReadOnlyDto public class EnvelopeReceiverReadOnlyDto
{ {
/// <summary> /// <summary>

View File

@@ -1,11 +1,8 @@
using Microsoft.AspNetCore.Mvc; namespace EnvelopeGenerator.Application.Common.Dto.EnvelopeReceiverReadOnly;
namespace EnvelopeGenerator.Application.Common.Dto.EnvelopeReceiverReadOnly;
/// <summary> /// <summary>
/// Data Transfer Object for updating a read-only envelope receiver. /// Data Transfer Object for updating a read-only envelope receiver.
/// </summary> /// </summary>
[ApiExplorerSettings(IgnoreApi = true)]
public class EnvelopeReceiverReadOnlyUpdateDto public class EnvelopeReceiverReadOnlyUpdateDto
{ {
/// <summary> /// <summary>

View File

@@ -1,11 +1,8 @@
using Microsoft.AspNetCore.Mvc; namespace EnvelopeGenerator.Application.Common.Dto;
namespace EnvelopeGenerator.Application.Common.Dto;
/// <summary> /// <summary>
/// Data Transfer Object representing a type of envelope with its configuration settings. /// Data Transfer Object representing a type of envelope with its configuration settings.
/// </summary> /// </summary>
[ApiExplorerSettings(IgnoreApi = true)]
public class EnvelopeTypeDto public class EnvelopeTypeDto
{ {
/// <summary> /// <summary>

View File

@@ -1,9 +1,6 @@
using Microsoft.AspNetCore.Mvc; namespace EnvelopeGenerator.Application.Common.Dto.Messaging;
namespace EnvelopeGenerator.Application.Common.Dto.Messaging;
/// <summary> /// <summary>
/// ///
/// </summary> /// </summary>
[ApiExplorerSettings(IgnoreApi = true)]
public class GtxMessagingResponse : Dictionary<string, object?> { } public class GtxMessagingResponse : Dictionary<string, object?> { }

View File

@@ -1,11 +1,8 @@
using Microsoft.AspNetCore.Mvc; namespace EnvelopeGenerator.Application.Common.Dto.Messaging;
namespace EnvelopeGenerator.Application.Common.Dto.Messaging;
/// <summary> /// <summary>
/// ///
/// </summary> /// </summary>
[ApiExplorerSettings(IgnoreApi = true)]
public record SmsResponse public record SmsResponse
{ {
/// <summary> /// <summary>

View File

@@ -0,0 +1,16 @@
namespace EnvelopeGenerator.Application.Common.Dto;
public sealed class PdfPageImageDto
{
public int PageNumber { get; init; }
public double WidthPt { get; init; }
public double HeightPt { get; init; }
public int WidthPx { get; init; }
public int HeightPx { get; init; }
public string ImageDataUrl { get; init; } = string.Empty;
}

View File

@@ -0,0 +1,6 @@
namespace EnvelopeGenerator.Application.Common.Dto;
public sealed class PdfRenderRequestDto
{
public byte[]? PdfBytes { get; init; }
}

View File

@@ -1,5 +1,4 @@
using EnvelopeGenerator.Application.Common.Dto.EnvelopeReceiver; using EnvelopeGenerator.Application.Common.Dto.EnvelopeReceiver;
using Microsoft.AspNetCore.Mvc;
using System.Text.Json.Serialization; using System.Text.Json.Serialization;
namespace EnvelopeGenerator.Application.Common.Dto.Receiver; namespace EnvelopeGenerator.Application.Common.Dto.Receiver;
@@ -7,7 +6,6 @@ namespace EnvelopeGenerator.Application.Common.Dto.Receiver;
/// <summary> /// <summary>
/// ///
/// </summary> /// </summary>
[ApiExplorerSettings(IgnoreApi = true)]
public class ReceiverDto public class ReceiverDto
{ {
/// <summary> /// <summary>

View File

@@ -0,0 +1,65 @@
namespace EnvelopeGenerator.Application.Common.Dto;
/// <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
/// </remarks>
public sealed record Signature
{
/// <summary>
/// TBDD_DOCUMENT_RECEIVER_ELEMENT.ID - identifies the specific signature field on the PDF page.
/// </summary>
public required int Id { get; init; }
/// <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>Example:</b> "Max Mustermann"
/// </summary>
public required string FullName { get; init; }
private readonly string? _position = null;
/// <summary>
/// Job title or position of the signer.
/// <br/>
/// <b>Required:</b> No (optional field)
/// <br/>
/// <b>Example:</b> "Geschäftsführer" or empty string
/// </summary>
public string? Position
{
get => _position;
init => _position = string.IsNullOrWhiteSpace(value) ? null : value;
}
/// <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; }
}

View File

@@ -0,0 +1,39 @@
namespace EnvelopeGenerator.Application.Common.Dto;
/// <summary>
/// Represents a user's UI preferences stored in the distributed cache (dbo.TBDD_CACHE).
/// </summary>
public sealed record UserPreferencesDto
{
/// <summary>
/// The active DevExpress theme name.
/// Allowed values: "blazing-berry", "fluent", "purple"
/// </summary>
public string ThemeName { get; set; } = "blazing-berry";
/// <summary>
/// Whether dark mode is active.
/// Default: false (light mode).
/// </summary>
public bool IsDarkMode { get; set; } = false;
/// <summary>
/// Persisted DxGrid layout states, keyed by a stable grid identifier.
/// Example keys: "sender.active-envelopes", "sender.completed-envelopes"
/// Values are opaque JSON strings produced by DxGrid.SaveLayoutAsync().
/// </summary>
public Dictionary<string, string> GridLayouts { get; set; } = new();
/// <summary>
/// The authenticated user's display name ("{GivenName} {Surname}").
/// Populated by the server on GET responses from JWT claims.
/// NOT persisted in cache — injected at read time by UserPreferencesController.
/// Ignored on PUT requests.
/// </summary>
public string? DisplayName { get; set; }
/// <summary>
///
/// </summary>
public static UserPreferencesDto Default { get; } = new UserPreferencesDto();
}

View File

@@ -1,5 +1,6 @@
using AutoMapper; using AutoMapper;
using EnvelopeGenerator.Domain.Interfaces.Auditing; using EnvelopeGenerator.Domain.Interfaces.Auditing;
using System;
namespace EnvelopeGenerator.Application.Common.Extensions; namespace EnvelopeGenerator.Application.Common.Extensions;
@@ -21,4 +22,21 @@ public static class AutoMapperAuditingExtensions
public static IMappingExpression<TSource, TDestination> MapChangedWhen<TSource, TDestination>(this IMappingExpression<TSource, TDestination> expression) public static IMappingExpression<TSource, TDestination> MapChangedWhen<TSource, TDestination>(this IMappingExpression<TSource, TDestination> expression)
where TDestination : IHasChangedWhen where TDestination : IHasChangedWhen
=> expression.ForMember(dest => dest.ChangedWhen, opt => opt.MapFrom(_ => DateTime.Now)); => expression.ForMember(dest => dest.ChangedWhen, opt => opt.MapFrom(_ => DateTime.Now));
/// <summary>
/// Converts a base64 data URL string to a byte array.
/// Handles data URLs in the format: "data:image/png;base64,iVBORw0KG..."
/// </summary>
/// <param name="dataUrl">The base64 data URL string from Canvas.toDataURL()</param>
/// <returns>The decoded byte array, or null if the input is null or empty</returns>
public static byte[]? MapDataUrlToRequiredBytes(this string dataUrl)
{
// Remove data URL prefix (e.g., "data:image/png;base64,")
var base64Index = dataUrl.IndexOf(',', StringComparison.Ordinal);
if (base64Index == -1)
throw new ArgumentException("Invalid data URL format. Unable to extract base64 data.", nameof(dataUrl));
var base64Data = dataUrl[(base64Index + 1)..];
return Convert.FromBase64String(base64Data);
}
} }

View File

@@ -0,0 +1,8 @@
using EnvelopeGenerator.Application.Common.Dto;
namespace EnvelopeGenerator.Application.Common.Interfaces.Doc;
public interface IPdfPageImageRenderer
{
Task<IReadOnlyList<PdfPageImageDto>> RenderPagesAsImagesAsync(byte[] pdfBytes, CancellationToken cancellationToken = default);
}

View File

@@ -0,0 +1,29 @@
using EnvelopeGenerator.Application.Common.Dto;
namespace EnvelopeGenerator.Application.Common.Interfaces.Services;
/// <summary>
/// Manages per-user UI preferences (theme, dark mode, grid layouts).
/// Preferences are stored in the distributed cache (dbo.TBDD_CACHE) as JSON.
/// Cache key schema: "signflow.user-preferences:{username}"
/// </summary>
public interface IUserPreferencesService
{
/// <summary>
/// Retrieves preferences for the given user.
/// Always returns a valid object — never throws, never returns null.
/// Returns defaults if no entry exists in cache.
/// </summary>
/// <param name="id"></param>
/// <param name="ct">Cancellation token.</param>
Task<UserPreferencesDto> GetOrCreateAsync(string id, CancellationToken ct = default);
/// <summary>
/// Persists the given preferences for the user in the distributed cache.
/// Failures are non-fatal and silently ignored.
/// </summary>
/// <param name="id"></param>
/// <param name="preferences">The preferences to persist.</param>
/// <param name="ct">Cancellation token.</param>
Task SaveAsync(string id, UserPreferencesDto preferences, CancellationToken ct = default);
}

View File

@@ -7,7 +7,9 @@ using EnvelopeGenerator.Application.Common.Dto.Receiver;
using EnvelopeGenerator.Application.Common.Extensions; using EnvelopeGenerator.Application.Common.Extensions;
using EnvelopeGenerator.Domain.Entities; using EnvelopeGenerator.Domain.Entities;
namespace EnvelopeGenerator.Application.Common.Dto; using EnvelopeGenerator.Application.Common.Dto;
namespace EnvelopeGenerator.Application.Common;
/// <summary> /// <summary>
/// Represents the AutoMapper profile configuration for mapping between /// Represents the AutoMapper profile configuration for mapping between
@@ -26,31 +28,37 @@ public class MappingProfile : Profile
CreateMap<DocReceiverElement, DocReceiverElementDto>(); CreateMap<DocReceiverElement, DocReceiverElementDto>();
CreateMap<DocumentStatus, DocumentStatusDto>(); CreateMap<DocumentStatus, DocumentStatusDto>();
CreateMap<EmailTemplate, EmailTemplateDto>(); CreateMap<EmailTemplate, EmailTemplateDto>();
CreateMap<Envelope, EnvelopeDto>(); CreateMap<Envelope, EnvelopeDto>()
.ForMember(dest => dest.EnvelopeTypeTitle,
opt => opt.MapFrom(src => src.Type != null ? src.Type.Title : null));
CreateMap<Document, DocumentDto>(); CreateMap<Document, DocumentDto>();
CreateMap<Domain.Entities.History, HistoryDto>().ForMember(dest => dest.ActionDate, opt => opt.MapFrom(src => src.ChangedWhen)); CreateMap<Document, DocumentDetailsDto>();
CreateMap<Domain.Entities.History, HistoryCreateDto>().ForMember(dest => dest.ActionDate, opt => opt.MapFrom(src => src.ChangedWhen)); CreateMap<History, HistoryDto>().ForMember(dest => dest.ActionDate, opt => opt.MapFrom(src => src.ChangedWhen));
CreateMap<Domain.Entities.EnvelopeReceiver, EnvelopeReceiverDto>(); CreateMap<History, HistoryCreateDto>().ForMember(dest => dest.ActionDate, opt => opt.MapFrom(src => src.ChangedWhen));
CreateMap<Domain.Entities.EnvelopeReceiver, EnvelopeReceiverSecretDto>(); CreateMap<EnvelopeReceiver, EnvelopeReceiverDto>();
CreateMap<EnvelopeReceiver, EnvelopeReceiverSecretDto>();
CreateMap<EnvelopeType, EnvelopeTypeDto>(); CreateMap<EnvelopeType, EnvelopeTypeDto>();
CreateMap<Domain.Entities.Receiver, ReceiverDto>(); CreateMap<Receiver, ReceiverDto>();
CreateMap<Domain.Entities.EnvelopeReceiverReadOnly, EnvelopeReceiverReadOnlyDto>(); CreateMap<EnvelopeReceiverReadOnly, EnvelopeReceiverReadOnlyDto>();
CreateMap<ElementAnnotation, AnnotationDto>(); CreateMap<ElementAnnotation, AnnotationDto>();
// DTO to Entity mappings // DTO to Entity mappings
CreateMap<ConfigDto, Config>(); CreateMap<ConfigDto, Config>();
CreateMap<DocReceiverElementDto, DocReceiverElement>(); CreateMap<DocReceiverElementDto, DocReceiverElement>();
CreateMap<Signature, DocReceiverElement>()
.ForMember(dest => dest.Ink, opt => opt.MapFrom(src => src.DataUrl.MapDataUrlToRequiredBytes()))
.MapChangedWhen();
CreateMap<DocumentStatusDto, DocumentStatus>(); CreateMap<DocumentStatusDto, DocumentStatus>();
CreateMap<EmailTemplateDto, EmailTemplate>(); CreateMap<EmailTemplateDto, EmailTemplate>();
CreateMap<EnvelopeDto, Envelope>(); CreateMap<EnvelopeDto, Envelope>();
CreateMap<DocumentDto, Document>(); CreateMap<DocumentDto, Document>();
CreateMap<HistoryDto, Domain.Entities.History>().ForMember(dest => dest.ChangedWhen, opt => opt.MapFrom(src => src.ActionDate)); CreateMap<HistoryDto, History>().ForMember(dest => dest.ChangedWhen, opt => opt.MapFrom(src => src.ActionDate));
CreateMap<HistoryCreateDto, Domain.Entities.History>().ForMember(dest => dest.ChangedWhen, opt => opt.MapFrom(src => src.ActionDate)); CreateMap<HistoryCreateDto, History>().ForMember(dest => dest.ChangedWhen, opt => opt.MapFrom(src => src.ActionDate));
CreateMap<EnvelopeReceiverDto, Domain.Entities.EnvelopeReceiver>(); CreateMap<EnvelopeReceiverDto, EnvelopeReceiver>();
CreateMap<EnvelopeTypeDto, EnvelopeType>(); CreateMap<EnvelopeTypeDto, EnvelopeType>();
CreateMap<ReceiverDto, Domain.Entities.Receiver>().ForMember(rcv => rcv.EnvelopeReceivers, rcvReadDto => rcvReadDto.Ignore()); CreateMap<ReceiverDto, Receiver>().ForMember(rcv => rcv.EnvelopeReceivers, rcvReadDto => rcvReadDto.Ignore());
CreateMap<EnvelopeReceiverReadOnlyCreateDto, Domain.Entities.EnvelopeReceiverReadOnly>(); CreateMap<EnvelopeReceiverReadOnlyCreateDto, EnvelopeReceiverReadOnly>();
CreateMap<EnvelopeReceiverReadOnlyUpdateDto, Domain.Entities.EnvelopeReceiverReadOnly>(); CreateMap<EnvelopeReceiverReadOnlyUpdateDto, EnvelopeReceiverReadOnly>();
CreateMap<AnnotationCreateDto, ElementAnnotation>() CreateMap<AnnotationCreateDto, ElementAnnotation>()
.MapAddedWhen(); .MapAddedWhen();
@@ -61,4 +69,4 @@ public class MappingProfile : Profile
? new SmsResponse() { Ok = true } ? new SmsResponse() { Ok = true }
: new SmsResponse() { Ok = false, Errors = gtxRes }); : new SmsResponse() { Ok = false, Errors = gtxRes });
} }
} }

View File

@@ -0,0 +1,21 @@
using EnvelopeGenerator.Domain.Constants;
using EnvelopeGenerator.Domain.Entities;
using MediatR;
namespace EnvelopeGenerator.Application.Common.Notifications.EnvelopeDeleted;
public record EnvelopeDeletedNotification : INotification, ISendMailNotification
{
public required Envelope Envelope { get; init; }
public required EnvelopeReceiver EnvelopeReceiver { get; init; }
public required string Reason { get; init; }
public required string SenderReference { get; init; }
public EmailTemplateType TemplateType => EmailTemplateType.DocumentDeleted;
public string EmailAddress => EnvelopeReceiver.Receiver?.EmailAddress
?? throw new InvalidOperationException("Receiver email is missing for EnvelopeDeletedNotification.");
}

View File

@@ -0,0 +1,42 @@
using DigitalData.Core.Abstraction.Application.Repository;
using DigitalData.EmailProfilerDispatcher.Abstraction.Entities;
using EnvelopeGenerator.Application.Common.Configurations;
using EnvelopeGenerator.Domain.Constants;
using EnvelopeGenerator.Domain.Entities;
using Microsoft.Extensions.Options;
namespace EnvelopeGenerator.Application.Common.Notifications.EnvelopeDeleted.Handlers;
public class SendDeletedMailHandler : SendMailHandler<EnvelopeDeletedNotification>
{
public SendDeletedMailHandler(
IRepository<EmailTemplate> tempRepo,
IRepository<EmailOut> emailOutRepo,
IOptions<MailParams> mailParamsOptions,
IOptions<DispatcherParams> dispatcherParamsOptions)
: base(tempRepo, emailOutRepo, mailParamsOptions, dispatcherParamsOptions)
{
}
protected override void ConfigureEmailOut(EnvelopeDeletedNotification notification, EmailOut emailOut)
{
emailOut.ReferenceString = notification.EmailAddress;
emailOut.ReferenceId = notification.EnvelopeReceiver.ReceiverId;
emailOut.WfId = (int)EnvelopeStatus.MessageDeletionSent;
}
protected override Dictionary<string, string> CreatePlaceHolders(EnvelopeDeletedNotification notification)
{
var senderName = notification.Envelope.User?.Email;
if (string.IsNullOrWhiteSpace(senderName))
senderName = notification.SenderReference;
return new Dictionary<string, string>
{
["[NAME_RECEIVER]"] = notification.EnvelopeReceiver.Name ?? string.Empty,
["[NAME_SENDER]"] = senderName ?? string.Empty,
["[DOCUMENT_TITLE]"] = notification.Envelope.Title ?? string.Empty,
["[REASON]"] = notification.Reason
};
}
}

View File

@@ -1,4 +1,6 @@
namespace EnvelopeGenerator.Application.Common.Query; using System.ComponentModel.DataAnnotations.Schema;
namespace EnvelopeGenerator.Application.Common.Query;
/// <summary> /// <summary>
/// Stellt eine Abfrage dar, um die Details eines Empfängers zu lesen. /// Stellt eine Abfrage dar, um die Details eines Empfängers zu lesen.
@@ -29,5 +31,6 @@ public record ReceiverQueryBase
/// <see cref="Id"/>, <see cref="EmailAddress"/>, or <see cref="Signature"/> is not null. /// <see cref="Id"/>, <see cref="EmailAddress"/>, or <see cref="Signature"/> is not null.
/// <para>Usage example: The query can be executed only if at least one criterion is specified.</para> /// <para>Usage example: The query can be executed only if at least one criterion is specified.</para>
/// </remarks> /// </remarks>
public bool HasAnyCriteria => Id is not null || EmailAddress is not null || Signature is not null; [NotMapped]
public virtual bool HasAnyCriteria => Id is not null || EmailAddress is not null || Signature is not null;
} }

View File

@@ -13,7 +13,7 @@ public class EnvelopeReceiverAddReadSQL : ISQL<Envelope>
/// ENV_UID, EMAIL_ADRESS, SALUTATION, PHONE, /// ENV_UID, EMAIL_ADRESS, SALUTATION, PHONE,
/// </summary> /// </summary>
public string Raw => @" public string Raw => @"
DECLARE @OUT_RECEIVER_ID int DECLARE @OUT_RECEIVER_ID bigint
EXEC [dbo].[PRSIG_API_CREATE_RECEIVER] EXEC [dbo].[PRSIG_API_CREATE_RECEIVER]
{0}, {0},

View File

@@ -10,6 +10,8 @@ using System.Reflection;
using MediatR; using MediatR;
using EnvelopeGenerator.Application.DocReceiverElements.Commands; using EnvelopeGenerator.Application.DocReceiverElements.Commands;
using EnvelopeGenerator.Application.DocReceiverElements.Behaviors; using EnvelopeGenerator.Application.DocReceiverElements.Behaviors;
using EnvelopeGenerator.Application.Common.Behaviors;
using FluentValidation;
namespace EnvelopeGenerator.Application; namespace EnvelopeGenerator.Application;
@@ -51,32 +53,40 @@ public static class DependencyInjection
services.Configure<TotpSmsParams>(config.GetSection(nameof(TotpSmsParams))); services.Configure<TotpSmsParams>(config.GetSection(nameof(TotpSmsParams)));
services.AddHttpClientService<GtxMessagingParams>(config.GetSection(nameof(GtxMessagingParams))); services.AddHttpClientService<GtxMessagingParams>(config.GetSection(nameof(GtxMessagingParams)));
services.TryAddSingleton<ISmsSender, GTXSmsSender>(); services.TryAddScoped<ISmsSender, GTXSmsSender>(); // Changed: Singleton → Scoped
services.TryAddSingleton<IEnvelopeSmsHandler, EnvelopeSmsHandler>(); services.TryAddScoped<IEnvelopeSmsHandler, EnvelopeSmsHandler>(); // Changed: Singleton → Scoped
services.TryAddSingleton<IAuthenticator, Authenticator>(); services.TryAddSingleton<IAuthenticator, Authenticator>();
services.TryAddSingleton<QRCodeGenerator>(); services.TryAddSingleton<QRCodeGenerator>();
services.AddMediatR(cfg => services.AddMediatR(cfg =>
{ {
cfg.RegisterServicesFromAssembly(Assembly.GetExecutingAssembly()); cfg.RegisterServicesFromAssembly(Assembly.GetExecutingAssembly());
// Generic request validation
cfg.AddOpenBehavior(typeof(ValidationBehavior<,>));
// Register SignCommand pipeline behaviors in execution order // Register SignCommand pipeline behaviors in execution order
// 0. EnvelopeReceiverResolutionBehavior - Resolves EnvelopeReceiver from query parameters (executes FIRST) // 0. EnvelopeReceiverResolutionBehavior - Resolves EnvelopeReceiver from query parameters (executes FIRST)
cfg.AddBehavior<IPipelineBehavior<SigningCommand, Unit>, EnvelopeReceiverResolutionBehavior>(); cfg.AddBehavior<IPipelineBehavior<SigningCommand, Unit>, EnvelopeReceiverResolutionBehavior>();
// 1. AnnotationBehavior - Saves annotations (executes second) // 1. AnnotationBehavior - Saves annotations (executes second)
cfg.AddBehavior<IPipelineBehavior<SigningCommand, Unit>, SaveSignatureBehavior>();
// 2. AnnotationBehavior - Saves annotations for legacy web flow
cfg.AddBehavior<IPipelineBehavior<SigningCommand, Unit>, AnnotationBehavior>(); cfg.AddBehavior<IPipelineBehavior<SigningCommand, Unit>, AnnotationBehavior>();
// 2. DocStatusBehavior - Creates document status (executes third) // 3. DocStatusBehavior - Creates document status
cfg.AddBehavior<IPipelineBehavior<SigningCommand, Unit>, DocStatusBehavior>(); cfg.AddBehavior<IPipelineBehavior<SigningCommand, Unit>, DocStatusBehavior>();
// 3. HistoryBehavior - Records history (executes fourth) // 4. HistoryBehavior - Records history
cfg.AddBehavior<IPipelineBehavior<SigningCommand, Unit>, HistoryBehavior>(); cfg.AddBehavior<IPipelineBehavior<SigningCommand, Unit>, HistoryBehavior>();
// 4. SendSignedMailBehavior - Sends notification email (executes LAST, only if all previous succeed) // 5. SendSignedMailBehavior - Sends notification email (executes LAST)
cfg.AddBehavior<IPipelineBehavior<SigningCommand, Unit>, SendSignedMailBehavior>(); cfg.AddBehavior<IPipelineBehavior<SigningCommand, Unit>, SendSignedMailBehavior>();
}); });
services.AddValidatorsFromAssembly(Assembly.GetExecutingAssembly());
return services; return services;
} }
} }

View File

@@ -1,4 +1,5 @@
using DigitalData.Core.Abstraction.Application.Repository; using DigitalData.Core.Abstraction.Application.Repository;
using DigitalData.Core.Exceptions;
using EnvelopeGenerator.Application.Common.Dto; using EnvelopeGenerator.Application.Common.Dto;
using EnvelopeGenerator.Application.DocReceiverElements.Commands; using EnvelopeGenerator.Application.DocReceiverElements.Commands;
using EnvelopeGenerator.Domain.Entities; using EnvelopeGenerator.Domain.Entities;
@@ -10,13 +11,13 @@ namespace EnvelopeGenerator.Application.DocReceiverElements.Behaviors;
/// Pipeline behavior that saves annotations. /// Pipeline behavior that saves annotations.
/// Executes first in the signing process. /// Executes first in the signing process.
/// </summary> /// </summary>
[Obsolete("This notification is deprecated. Use Signature.Commands.SignCommand instead.")] [Obsolete("The PSPDFKit library is deprecated.")]
public class AnnotationBehavior : IPipelineBehavior<SigningCommand, Unit> public class AnnotationBehavior : IPipelineBehavior<SigningCommand, Unit>
{ {
private readonly IRepository<ElementAnnotation> _repo; private readonly IRepository<ElementAnnotation> _repo;
/// <summary> /// <summary>
/// /// Initializes a new instance of the <see cref="AnnotationBehavior"/> class.
/// </summary> /// </summary>
/// <param name="repository"></param> /// <param name="repository"></param>
public AnnotationBehavior(IRepository<ElementAnnotation> repository) public AnnotationBehavior(IRepository<ElementAnnotation> repository)
@@ -29,13 +30,21 @@ public class AnnotationBehavior : IPipelineBehavior<SigningCommand, Unit>
/// </summary> /// </summary>
/// <param name="request"></param> /// <param name="request"></param>
/// <param name="next"></param> /// <param name="next"></param>
/// <param name="cancellationToken"></param> /// <param name="cancel"></param>
/// <returns></returns> /// <returns></returns>
public async Task<Unit> Handle(SigningCommand request, RequestHandlerDelegate<Unit> next, CancellationToken cancellationToken) public async Task<Unit> Handle(SigningCommand request, RequestHandlerDelegate<Unit> next, CancellationToken cancel)
{ {
if (request.PsPdfKitAnnotation is PsPdfKitAnnotation annot) if(request.ReceiverAppType != ReceiverAppType.LegacyWeb)
await _repo.CreateAsync(annot.Structured, cancellationToken); if(request.PsPdfKitAnnotation is null)
return await next(cancel);
else
throw new BadRequestException("PsPdfKit Annotation are only supported for the legacy web receiver type.");
return await next(cancellationToken); if (request.PsPdfKitAnnotation is PsPdfKitAnnotation annot)
await _repo.CreateAsync(annot.Structured, cancel);
else
throw new BadRequestException("Annotation data is missing or invalid.");
return await next(cancel);
} }
} }

View File

@@ -43,7 +43,11 @@ public class EnvelopeReceiverResolutionBehavior : IPipelineBehavior<SigningComma
// If EnvelopeReceiver is not provided, query it from database // If EnvelopeReceiver is not provided, query it from database
if (request.EnvelopeReceiver is null) if (request.EnvelopeReceiver is null)
{ {
var er = await _erRepo.Query.Where(request, notnull: true).SingleOrDefaultAsync(cancellationToken) var er = await _erRepo.Query
.Where(request, notnull: true)
.Include(x => x.Envelope)
.Include(x => x.Receiver)
.SingleOrDefaultAsync(cancellationToken)
?? throw new NotFoundException("EnvelopeReceiver not found"); ?? throw new NotFoundException("EnvelopeReceiver not found");
request.SetEnvelopeReceiver(_mapper.Map<EnvelopeReceiverDto>(er)); request.SetEnvelopeReceiver(_mapper.Map<EnvelopeReceiverDto>(er));

View File

@@ -1,22 +1,11 @@
using AutoMapper;
using DigitalData.Core.Abstraction.Application.Repository;
using DigitalData.Core.Exceptions;
using EnvelopeGenerator.Application.Common.Dto; using EnvelopeGenerator.Application.Common.Dto;
using EnvelopeGenerator.Application.DocStatus.Commands; using EnvelopeGenerator.Application.DocReceiverElements.Commands;
using EnvelopeGenerator.Domain.Constants; using EnvelopeGenerator.Domain.Entities;
using MediatR; using MediatR;
using System.Text.Json; using Microsoft.EntityFrameworkCore;
<<<<<<< TODO: Unmerged change from project 'EnvelopeGenerator.Application (net8.0)', Before:
=======
using EnvelopeGenerator.Application.DocReceiverElements.Commands.SigningCommand.SigningCommand;
using EnvelopeGenerator.Application.DocReceiverElements.Commands.SigningCommand;
>>>>>>> After
<<<<<<< TODO: Unmerged change from project 'EnvelopeGenerator.Application (net9.0)', Before:
=======
using EnvelopeGenerator.Application.DocReceiverElements.Commands.SigningCommand;
>>>>>>> After
using EnvelopeGenerator.Application.DocReceiverElements.Commands.SigningCommand.SigningCommand.SigningCommand;
using EnvelopeGenerator.Application.DocReceiverElements.Commands.SigningCommand.SigningCommand;
using EnvelopeGenerator.Application.DocReceiverElements.Commands.SigningCommand;
namespace EnvelopeGenerator.Application.DocReceiverElements.Behaviors; namespace EnvelopeGenerator.Application.DocReceiverElements.Behaviors;
@@ -28,13 +17,22 @@ public class SaveSignatureBehavior : IPipelineBehavior<SigningCommand, Unit>
{ {
private readonly ISender _sender; private readonly ISender _sender;
private readonly IRepository<DocReceiverElement> _elementRepo;
private readonly IMapper _mapper;
/// <summary> /// <summary>
/// ///
/// </summary> /// </summary>
/// <param name="sender"></param> /// <param name="sender"></param>
public SaveSignatureBehavior(ISender sender) /// <param name="elementRepo"></param>
/// <param name="mapper"></param>
public SaveSignatureBehavior(ISender sender, IRepository<DocReceiverElement> elementRepo, IMapper mapper)
{ {
_sender = sender; _sender = sender;
_elementRepo = elementRepo;
_elementRepo = elementRepo;
_mapper = mapper;
} }
/// <summary> /// <summary>
@@ -42,10 +40,31 @@ public class SaveSignatureBehavior : IPipelineBehavior<SigningCommand, Unit>
/// </summary> /// </summary>
/// <param name="request"></param> /// <param name="request"></param>
/// <param name="next"></param> /// <param name="next"></param>
/// <param name="cancellationToken"></param> /// <param name="cancel"></param>
/// <returns></returns> /// <returns></returns>
public async Task<Unit> Handle(SigningCommand request, RequestHandlerDelegate<Unit> next, CancellationToken cancellationToken) public async Task<Unit> Handle(SigningCommand request, RequestHandlerDelegate<Unit> next, CancellationToken cancel)
{ {
return await next(cancellationToken); if (request.ReceiverAppType == ReceiverAppType.LegacyWeb)
return await next(cancel);
else if(request.Signatures is not IEnumerable<Signature> signatures)
throw new BadRequestException($"Signatures are required for saving signature behavior.");
var elements = await _elementRepo
.Where(e => e.Document.EnvelopeId == request.EnvelopeReceiver.EnvelopeId)
.Where(e => e.ReceiverId == request.EnvelopeReceiver.ReceiverId)
.ToListAsync(cancel);
foreach (var element in elements)
{
var signatures = request.Signatures.Where(s => s.Id == element.Id).ToList();
if(signatures.Count == 0)
throw new BadRequestException("No signature found for element with id {element.Id}.");
else if(signatures.Count > 1)
throw new BadRequestException("Multiple signatures found for element with id {element.Id}.");
await _elementRepo.UpdateAsync(signatures.First(), e => e.Id == element.Id, cancel);
}
return await next(cancel);
} }
} }

View File

@@ -5,70 +5,6 @@ using EnvelopeGenerator.Application.Common.Query;
namespace EnvelopeGenerator.Application.DocReceiverElements.Commands; namespace EnvelopeGenerator.Application.DocReceiverElements.Commands;
/// <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
/// </remarks>
public sealed record SignatureDto
{
/// <summary>
/// TBDD_DOCUMENT_RECEIVER_ELEMENT.ID - identifies the specific signature field on the PDF page.
/// </summary>
public required int ElementId { get; init; }
/// <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>Example:</b> "Max Mustermann"
/// </summary>
public required string FullName { get; init; }
private readonly string? _position = null;
/// <summary>
/// Job title or position of the signer.
/// <br/>
/// <b>Required:</b> No (optional field)
/// <br/>
/// <b>Example:</b> "Geschäftsführer" or empty string
/// </summary>
public string? Position
{
get => _position;
init => _position = string.IsNullOrWhiteSpace(value) ? value : null;
}
/// <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; }
}
/// <summary> /// <summary>
/// Command to sign a document by a receiver. /// Command to sign a document by a receiver.
/// </summary> /// </summary>
@@ -99,7 +35,12 @@ public record SigningCommand : EnvelopeReceiverQueryBase, IRequest
/// <summary> /// <summary>
/// ///
/// </summary> /// </summary>
public IEnumerable<SignatureDto>? Signatures { get; init; } public IEnumerable<Signature>? Signatures { get; init; }
/// <summary>
///
/// </summary>
public ReceiverAppType ReceiverAppType { get; init; } = ReceiverAppType.ReceiverUI;
} }
/// <summary> /// <summary>
@@ -118,4 +59,20 @@ public class SignCommandHandler : IRequestHandler<SigningCommand>
{ {
return Task.CompletedTask; return Task.CompletedTask;
} }
}
/// <summary>
///
/// </summary>
public enum ReceiverAppType
{
/// <summary>
///
/// </summary>
ReceiverUI = 0,
/// <summary>
///
/// </summary>
LegacyWeb = 1,
} }

View File

@@ -1,19 +0,0 @@
using AutoMapper;
using EnvelopeGenerator.Application.DocReceiverElements.Commands;
using EnvelopeGenerator.Domain.Entities;
namespace EnvelopeGenerator.Application.DocReceiverElements;
/// <summary>
///
/// </summary>
public class MappingProfile : Profile
{
/// <summary>
///
/// </summary>
public MappingProfile()
{
CreateMap<SignatureDto, DocReceiverElement>();
}
}

View File

@@ -0,0 +1,66 @@
using AutoMapper;
using EnvelopeGenerator.Application.Common.Dto;
using EnvelopeGenerator.Application.Common.Query;
using EnvelopeGenerator.Domain.Entities;
using MediatR;
using EnvelopeGenerator.Application.Common.Extensions;
using DigitalData.Core.Abstraction.Application.Repository;
using Microsoft.EntityFrameworkCore;
namespace EnvelopeGenerator.Application.DocReceiverElements.Queries;
/// <summary>
///
/// </summary>
public record ReadDocReceiverElementQuery : EnvelopeReceiverQueryBase, IRequest<IEnumerable<DocReceiverElementDto>>
{
}
/// <summary>
///
/// </summary>
public class ReadDocReceiverElementQueryHandler : IRequestHandler<ReadDocReceiverElementQuery, IEnumerable<DocReceiverElementDto>>
{
private readonly IRepository<DocReceiverElement> _repository;
private readonly IMapper _mapper;
/// <summary>
///
/// </summary>
/// <param name="repository"></param>
/// <param name="mapper"></param>
public ReadDocReceiverElementQueryHandler(IRepository<DocReceiverElement> repository, IMapper mapper)
{
_repository = repository;
_mapper = mapper;
}
/// <summary>
///
/// </summary>
/// <param name="request"></param>
/// <param name="cancellationToken"></param>
/// <returns></returns>
/// <exception cref="NotImplementedException"></exception>
public async Task<IEnumerable<DocReceiverElementDto>> Handle(ReadDocReceiverElementQuery request, CancellationToken cancellationToken)
{
var q = _repository.Query;
if(request.Envelope.Id is int envelopeId)
q = q.Where(e => e.Document.EnvelopeId == envelopeId);
if (request.Envelope.Uuid is string envelopeUuid)
q = q.Where(e => e.Document.Envelope.Uuid == envelopeUuid);
if (request.Receiver.Id is int receiverId)
q = q.Where(e => e.ReceiverId == receiverId);
if (request.Receiver.Signature is string signature)
q = q.Where(e => e.Receiver.Signature == signature);
var elements = await q.ToListAsync(cancellationToken);
return _mapper.Map<IEnumerable<DocReceiverElementDto>>(elements);
}
}

View File

@@ -0,0 +1,44 @@
using AutoMapper;
using AutoMapper.QueryableExtensions;
using DigitalData.Core.Abstraction.Application.Repository;
using EnvelopeGenerator.Application.Common.Dto;
using EnvelopeGenerator.Domain.Entities;
using MediatR;
using Microsoft.EntityFrameworkCore;
namespace EnvelopeGenerator.Application.Documents.Queries;
/// <summary>
/// Represents a query to read document metadata/details without binary payload.
/// </summary>
/// <param name="EnvelopeId">The identifier of the envelope associated with the document.</param>
/// <param name="UserId">The unique identifier of the sending user. Optional.</param>
public record ReadDocumentDetailsQuery(int EnvelopeId, int? UserId = null) : IRequest<DocumentDetailsDto?>;
/// <summary>
/// Handles queries for reading document details without loading BYTE_DATA.
/// </summary>
public class ReadDocumentDetailsQueryHandler : IRequestHandler<ReadDocumentDetailsQuery, DocumentDetailsDto?>
{
private readonly IRepository<Document> _repo;
private readonly IMapper _mapper;
public ReadDocumentDetailsQueryHandler(IRepository<Document> repo, IMapper mapper)
{
_repo = repo;
_mapper = mapper;
}
public async Task<DocumentDetailsDto?> Handle(ReadDocumentDetailsQuery query, CancellationToken cancel)
{
var docQuery = _repo.Query.AsNoTracking();
if (query.UserId is int userId)
docQuery = docQuery.Where(doc => doc.Envelope.UserId == userId);
return await docQuery
.Where(d => d.EnvelopeId == query.EnvelopeId)
.ProjectTo<DocumentDetailsDto>(_mapper.ConfigurationProvider)
.FirstOrDefaultAsync(cancel);
}
}

View File

@@ -0,0 +1,13 @@
using FluentValidation;
namespace EnvelopeGenerator.Application.Documents.Queries;
public class ReadDocumentDetailsQueryValidator : AbstractValidator<ReadDocumentDetailsQuery>
{
public ReadDocumentDetailsQueryValidator()
{
RuleFor(x => x.EnvelopeId)
.GreaterThan(0)
.WithMessage("EnvelopeId is required.");
}
}

View File

@@ -13,7 +13,8 @@ namespace EnvelopeGenerator.Application.Documents.Queries;
/// </summary> /// </summary>
/// <param name="Id">The unique identifier of the document. Optional.</param> /// <param name="Id">The unique identifier of the document. Optional.</param>
/// <param name="EnvelopeId">The identifier of the envelope associated with the document. Optional.</param> /// <param name="EnvelopeId">The identifier of the envelope associated with the document. Optional.</param>
public record ReadDocumentQuery(int? Id = null, int? EnvelopeId = null) : IRequest<DocumentDto> /// <param name="UserId">The unique identifier of the sending user. Optional.</param>
public record ReadDocumentQuery(int? Id = null, int? EnvelopeId = null, int? UserId = null) : IRequest<DocumentDto>
{ {
} }
@@ -53,7 +54,10 @@ public class ReadDocumentQueryHandler : IRequestHandler<ReadDocumentQuery, Docum
/// </exception> /// </exception>
public async Task<DocumentDto> Handle(ReadDocumentQuery query, CancellationToken cancel) public async Task<DocumentDto> Handle(ReadDocumentQuery query, CancellationToken cancel)
{ {
var docQuery = _repo.Query.Include(doc => doc.Elements).ThenInclude(e => e.Annotations); var docQuery = _repo.Query.Include(doc => doc.Elements).ThenInclude(e => e.Annotations).AsNoTracking();
if (query.UserId is int userId)
docQuery = docQuery.Where(doc => doc.Envelope.UserId == userId);
if (query.Id is not null) if (query.Id is not null)
{ {

View File

@@ -0,0 +1,23 @@
using FluentValidation;
namespace EnvelopeGenerator.Application.Documents.Queries;
public class ReadDocumentQueryValidator : AbstractValidator<ReadDocumentQuery>
{
public ReadDocumentQueryValidator()
{
RuleFor(x => x)
.Must(x => (x.Id ?? 0) > 0 || (x.EnvelopeId ?? 0) > 0)
.WithMessage("Id or EnvelopeId is required.");
RuleFor(x => x.Id)
.GreaterThan(0)
.When(x => x.Id.HasValue)
.WithMessage("Id must be greater than 0.");
RuleFor(x => x.EnvelopeId)
.GreaterThan(0)
.When(x => x.EnvelopeId.HasValue)
.WithMessage("EnvelopeId must be greater than 0.");
}
}

View File

@@ -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>
@@ -19,9 +19,10 @@
<PackageReference Include="DigitalData.Core.Client" Version="2.1.0" /> <PackageReference Include="DigitalData.Core.Client" Version="2.1.0" />
<PackageReference Include="DigitalData.Core.Exceptions" Version="1.1.0" /> <PackageReference Include="DigitalData.Core.Exceptions" Version="1.1.0" />
<PackageReference Include="DigitalData.EmailProfilerDispatcher" Version="3.1.1" /> <PackageReference Include="DigitalData.EmailProfilerDispatcher" Version="3.1.1" />
<PackageReference Include="FluentValidation" Version="11.11.0" />
<PackageReference Include="FluentValidation.DependencyInjectionExtensions" Version="11.11.0" />
<PackageReference Include="HtmlSanitizer" Version="9.0.892" /> <PackageReference Include="HtmlSanitizer" Version="9.0.892" />
<PackageReference Include="MediatR" Version="12.5.0" /> <PackageReference Include="MediatR" Version="12.5.0" />
<PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="7.0.18" />
<PackageReference Include="Microsoft.Identity.Client" Version="4.82.1" /> <PackageReference Include="Microsoft.Identity.Client" Version="4.82.1" />
<PackageReference Include="Otp.NET" Version="1.4.0" /> <PackageReference Include="Otp.NET" Version="1.4.0" />
<PackageReference Include="QRCoder" Version="1.6.0" /> <PackageReference Include="QRCoder" Version="1.6.0" />
@@ -79,25 +80,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>

View File

@@ -1,6 +1,5 @@
using EnvelopeGenerator.Application.Common.Dto; using EnvelopeGenerator.Application.Common.Dto;
using MediatR; using MediatR;
using AutoMapper;
using DigitalData.Core.Abstraction.Application.Repository; using DigitalData.Core.Abstraction.Application.Repository;
using EnvelopeGenerator.Domain.Entities; using EnvelopeGenerator.Domain.Entities;
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
@@ -18,17 +17,14 @@ public record ReadEnvelopeTypesQuery : IRequest<IEnumerable<EnvelopeTypeDto>>;
public class ReadEnvelopeTypesQueryHandler : IRequestHandler<ReadEnvelopeTypesQuery, IEnumerable<EnvelopeTypeDto>> public class ReadEnvelopeTypesQueryHandler : IRequestHandler<ReadEnvelopeTypesQuery, IEnumerable<EnvelopeTypeDto>>
{ {
private readonly IRepository<EnvelopeType> _repository; private readonly IRepository<EnvelopeType> _repository;
private readonly IMapper _mapper;
/// <summary> /// <summary>
/// ///
/// </summary> /// </summary>
/// <param name="repository"></param> /// <param name="repository"></param>
/// <param name="mapper"></param> public ReadEnvelopeTypesQueryHandler(IRepository<EnvelopeType> repository)
public ReadEnvelopeTypesQueryHandler(IRepository<EnvelopeType> repository, IMapper mapper)
{ {
_repository = repository; _repository = repository;
_mapper = mapper;
} }
/// <summary> /// <summary>
@@ -39,7 +35,28 @@ public class ReadEnvelopeTypesQueryHandler : IRequestHandler<ReadEnvelopeTypesQu
/// <returns></returns> /// <returns></returns>
public async Task<IEnumerable<EnvelopeTypeDto>> Handle(ReadEnvelopeTypesQuery request, CancellationToken cancellationToken) public async Task<IEnumerable<EnvelopeTypeDto>> Handle(ReadEnvelopeTypesQuery request, CancellationToken cancellationToken)
{ {
var types = await _repository.Query.ToListAsync(cancellationToken); // TODO: Temporary manual projection to avoid null-materialization crashes from legacy nullable DB columns in EnvelopeType.
return _mapper.Map<IEnumerable<EnvelopeTypeDto>>(types); // Revisit and switch to AutoMapper query projection (ProjectTo) after schema nullability and mapping contracts are stabilized.
return await _repository.Query
.AsNoTracking()
.Select(t => new EnvelopeTypeDto
{
Id = t.Id,
Title = t.Title ?? string.Empty,
Language = string.IsNullOrWhiteSpace(t.Language) ? "de-DE" : t.Language,
ExpiresDays = t.ExpiresDays,
CertificationType = (int?)t.CertificationType,
UseAccessCode = t.UseAccessCode,
FinalEmailToCreator = (int?)t.FinalEmailToCreator,
FinalEmailToReceivers = (int?)t.FinalEmailToReceivers,
AddedWhen = t.AddedWhen,
ChangedWhen = t.ChangedWhen,
ExpiresWarningDays = t.ExpiresWarningDays,
SendReminderEmails = t.SendReminderEmails,
FirstReminderDays = t.FirstReminderDays,
ReminderIntervalDays = t.ReminderIntervalDays,
ContractType = t.ContractType
})
.ToListAsync(cancellationToken);
} }
} }

View File

@@ -1,5 +1,4 @@
using DigitalData.Core.Abstraction.Application.Repository; using EnvelopeGenerator.Application.Common.Dto;
using EnvelopeGenerator.Application.Common.Dto;
using EnvelopeGenerator.Application.Common.Interfaces.SQLExecutor; using EnvelopeGenerator.Application.Common.Interfaces.SQLExecutor;
using MediatR; using MediatR;
using System.ComponentModel.DataAnnotations; using System.ComponentModel.DataAnnotations;
@@ -13,23 +12,96 @@ namespace EnvelopeGenerator.Application.Envelopes.Commands;
/// </summary> /// </summary>
public record CreateEnvelopeCommand : IRequest<EnvelopeDto?> public record CreateEnvelopeCommand : IRequest<EnvelopeDto?>
{ {
/// <summary>
/// Optionale Umschlag-ID fuer Aktualisierungsvorgaenge.
/// </summary>
public int? EnvelopeId { get; init; }
/// <summary>
/// Kennung des ausgewaehlten Umschlagtyps (Vorlage).
/// </summary>
public int? EnvelopeTypeId { get; set; }
/// <summary>
/// Sprachcode fuer Umschlaginhalt und Benachrichtigungen.
/// </summary>
public string? Language { get; set; }
/// <summary>
/// Gibt an, ob der Zugriffscode fuer Empfaenger aktiviert ist.
/// </summary>
public bool? UseAccessCode { get; set; }
/// <summary>
/// Zertifizierungstyp fuer den Signaturprozess.
/// </summary>
public int? CertificationType { get; set; }
/// <summary>
/// Finale E-Mail-Einstellung fuer den Ersteller.
/// </summary>
public int? FinalEmailToCreator { get; set; }
/// <summary>
/// Finale E-Mail-Einstellung fuer die Empfaenger.
/// </summary>
public int? FinalEmailToReceivers { get; set; }
/// <summary>
/// Gibt an, ob Erinnerungs-E-Mails aktiviert sind.
/// </summary>
public bool? SendReminderEmails { get; set; }
/// <summary>
/// Anzahl der Tage bis zur ersten Erinnerung.
/// </summary>
public int? FirstReminderDays { get; set; }
/// <summary>
/// Intervall in Tagen zwischen Erinnerungs-E-Mails.
/// </summary>
public int? ReminderIntervalDays { get; set; }
/// <summary>
/// Anzahl der Tage bis zum Ablauf des Umschlags.
/// </summary>
public int? ExpiresWhenDays { get; set; }
/// <summary>
/// Anzahl der Tage vor Ablauf, ab denen gewarnt wird.
/// </summary>
public int? ExpiresWarningWhenDays { get; set; }
/// <summary> /// <summary>
/// Der Titel des Umschlags. Dies ist ein Pflichtfeld. /// Der Titel des Umschlags. Dies ist ein Pflichtfeld.
/// </summary> /// </summary>
[Required] public string? Title { get; set; }
public required string Title { get; set; }
/// <summary> /// <summary>
/// Die Nachricht, die im Umschlag enthalten sein soll. Dies ist ein Pflichtfeld. /// Die Nachricht, die im Umschlag enthalten sein soll. Dies ist ein Pflichtfeld.
/// </summary> /// </summary>
[Required] public string? Message { get; set; }
public required string Message { get; set; }
/// <summary> /// <summary>
/// Gibt an, ob die Zwei-Faktor-Authentifizierung für den Umschlag aktiviert ist. Standardmäßig false. /// Gibt an, ob die Zwei-Faktor-Authentifizierung für den Umschlag aktiviert ist. Standardmäßig false.
/// </summary> /// </summary>
public bool TFAEnabled { get; set; } = false; public bool TFAEnabled { get; set; } = false;
/// <summary>
/// Gibt an, ob der Umschlag nach dem Speichern sofort versendet werden soll.
/// </summary>
public bool Send { get; init; }
/// <summary>
/// Optionaler PDF-Dokumentinhalt als Base64.
/// </summary>
public CreateEnvelopeDocumentDto? Document { get; init; }
/// <summary>
/// Empfaengerliste inklusive optionaler Signaturfeld-Koordinaten.
/// </summary>
public List<CreateEnvelopeReceiverDto> Receivers { get; init; } = new();
/// <summary> /// <summary>
/// ID des Absenders /// ID des Absenders
/// </summary> /// </summary>
@@ -55,4 +127,14 @@ public record CreateEnvelopeCommand : IRequest<EnvelopeDto?>
[JsonIgnore] [JsonIgnore]
[NotMapped] [NotMapped]
public bool UseSQLExecutor { get; set; } = true; public bool UseSQLExecutor { get; set; } = true;
} }
public record CreateEnvelopeDocumentDto(string DataAsBase64);
public record CreateEnvelopeFieldDto(double X, double Y, int Page);
public record CreateEnvelopeReceiverDto(
string EmailAddress,
string? Name,
string? PhoneNumber,
List<CreateEnvelopeFieldDto> Fields);

View File

@@ -1,10 +1,20 @@
using AutoMapper; using AutoMapper;
using MediatR; using MediatR;
using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Options;
using DigitalData.Core.Abstraction.Application.Repository; using DigitalData.Core.Abstraction.Application.Repository;
using DigitalData.EmailProfilerDispatcher.Abstraction.Entities;
using DigitalData.Core.Exceptions;
using EnvelopeGenerator.Domain.Entities; using EnvelopeGenerator.Domain.Entities;
using EnvelopeGenerator.Application.Common.Dto; using EnvelopeGenerator.Application.Common.Dto;
using EnvelopeGenerator.Application.Common.Configurations;
using EnvelopeGenerator.Application.Common.Extensions;
using EnvelopeGenerator.Application.Common.Interfaces.Services;
using EnvelopeGenerator.Application.Common.Interfaces.SQLExecutor; using EnvelopeGenerator.Application.Common.Interfaces.SQLExecutor;
using EnvelopeGenerator.Application.Histories.Commands;
using EnvelopeGenerator.Domain.Constants;
using EnvelopeGenerator.Domain;
using Microsoft.EntityFrameworkCore;
namespace EnvelopeGenerator.Application.Envelopes.Commands; namespace EnvelopeGenerator.Application.Envelopes.Commands;
@@ -19,6 +29,28 @@ public class CreateEnvelopeCommandHandler : IRequestHandler<CreateEnvelopeComman
private IRepository<Envelope> Repository => _provider.GetRequiredService<IRepository<Envelope>>(); private IRepository<Envelope> Repository => _provider.GetRequiredService<IRepository<Envelope>>();
private IRepository<Document> DocumentRepository => _provider.GetRequiredService<IRepository<Document>>();
private IRepository<EnvelopeReceiver> EnvelopeReceiverRepository => _provider.GetRequiredService<IRepository<EnvelopeReceiver>>();
private IRepository<EnvelopeType> EnvelopeTypeRepository => _provider.GetRequiredService<IRepository<EnvelopeType>>();
private IRepository<Receiver> ReceiverRepository => _provider.GetRequiredService<IRepository<Receiver>>();
private IRepository<DocReceiverElement> DocReceiverElementRepository => _provider.GetRequiredService<IRepository<DocReceiverElement>>();
private IRepository<EmailTemplate> EmailTemplateRepository => _provider.GetRequiredService<IRepository<EmailTemplate>>();
private IRepository<EmailOut> EmailOutRepository => _provider.GetRequiredService<IRepository<EmailOut>>();
private MailParams MailParams => _provider.GetRequiredService<IOptions<MailParams>>().Value;
private DispatcherParams DispatcherParams => _provider.GetRequiredService<IOptions<DispatcherParams>>().Value;
private IConfigService ConfigService => _provider.GetRequiredService<IConfigService>();
private IMediator Mediator => _provider.GetRequiredService<IMediator>();
private readonly IMapper _mapper; private readonly IMapper _mapper;
/// <summary> /// <summary>
@@ -40,10 +72,572 @@ public class CreateEnvelopeCommandHandler : IRequestHandler<CreateEnvelopeComman
/// <returns></returns> /// <returns></returns>
public async Task<EnvelopeDto?> Handle(CreateEnvelopeCommand request, CancellationToken cancel) public async Task<EnvelopeDto?> Handle(CreateEnvelopeCommand request, CancellationToken cancel)
{ {
var envelope = request.UseSQLExecutor var now = DateTime.Now;
? await Executor.CreateEnvelopeAsync(request.UserId, request.Title, request.Message, request.TFAEnabled, cancel) var title = request.Title?.Trim() ?? string.Empty;
: await Repository.CreateAsync(request, cancel); var message = request.Message?.Trim() ?? string.Empty;
var settings = await ResolveEnvelopeSettingsAsync(request, existing: null, cancel);
Envelope envelope;
if (request.EnvelopeId is int envelopeId)
{
envelope = await Repository.Query
.Include(e => e.User)
.FirstOrDefaultAsync(e => e.Id == envelopeId, cancel)
?? throw new BadRequestException($"Envelope {envelopeId} not found.");
if (envelope.UserId != request.UserId)
throw new ForbiddenException();
settings = await ResolveEnvelopeSettingsAsync(request, envelope, cancel);
await Repository.UpdateAsync(e =>
{
e.Title = title;
e.Message = message;
e.TfaEnabled = request.TFAEnabled;
e.EnvelopeTypeId = settings.EnvelopeTypeId;
e.Language = settings.Language;
e.UseAccessCode = settings.UseAccessCode;
e.CertificationType = settings.CertificationType;
e.FinalEmailToCreator = settings.FinalEmailToCreator;
e.FinalEmailToReceivers = settings.FinalEmailToReceivers;
e.SendReminderEmails = settings.SendReminderEmails;
e.FirstReminderDays = settings.FirstReminderDays;
e.ReminderIntervalDays = settings.ReminderIntervalDays;
e.ExpiresWhenDays = settings.ExpiresWhenDays;
e.ExpiresWarningWhenDays = settings.ExpiresWarningWhenDays;
e.ContractType = settings.ContractType;
e.ExpiresWhen = settings.ExpiresWhen;
e.ExpiresWarningWhen = settings.ExpiresWarningWhen;
e.Status = EnvelopeStatus.EnvelopeSaved;
e.ChangedWhen = now;
}, e => e.Id == envelopeId, cancel);
envelope.Title = title;
envelope.Message = message;
envelope.TfaEnabled = request.TFAEnabled;
envelope.EnvelopeTypeId = settings.EnvelopeTypeId;
envelope.Language = settings.Language;
envelope.UseAccessCode = settings.UseAccessCode;
envelope.CertificationType = settings.CertificationType;
envelope.FinalEmailToCreator = settings.FinalEmailToCreator;
envelope.FinalEmailToReceivers = settings.FinalEmailToReceivers;
envelope.SendReminderEmails = settings.SendReminderEmails;
envelope.FirstReminderDays = settings.FirstReminderDays;
envelope.ReminderIntervalDays = settings.ReminderIntervalDays;
envelope.ExpiresWhenDays = settings.ExpiresWhenDays;
envelope.ExpiresWarningWhenDays = settings.ExpiresWarningWhenDays;
envelope.ContractType = settings.ContractType;
envelope.ExpiresWhen = settings.ExpiresWhen;
envelope.ExpiresWarningWhen = settings.ExpiresWarningWhen;
envelope.Status = EnvelopeStatus.EnvelopeSaved;
envelope.ChangedWhen = now;
}
else
{
envelope = request.UseSQLExecutor
? await Executor.CreateEnvelopeAsync(request.UserId, title, message, request.TFAEnabled, cancel)
: await Repository.CreateAsync(new Envelope
{
UserId = request.UserId,
Status = EnvelopeStatus.EnvelopeCreated,
Uuid = Guid.NewGuid().ToString(),
Title = title,
Message = message,
AddedWhen = now,
ChangedWhen = now,
Comment = string.Empty,
EnvelopeTypeId = settings.EnvelopeTypeId,
Language = settings.Language,
TfaEnabled = request.TFAEnabled,
UseAccessCode = settings.UseAccessCode,
CertificationType = settings.CertificationType,
FinalEmailToCreator = settings.FinalEmailToCreator,
FinalEmailToReceivers = settings.FinalEmailToReceivers,
SendReminderEmails = settings.SendReminderEmails,
FirstReminderDays = settings.FirstReminderDays,
ReminderIntervalDays = settings.ReminderIntervalDays,
ExpiresWhenDays = settings.ExpiresWhenDays,
ExpiresWarningWhenDays = settings.ExpiresWarningWhenDays,
ExpiresWhen = settings.ExpiresWhen,
ExpiresWarningWhen = settings.ExpiresWarningWhen,
ContractType = settings.ContractType
}, cancel);
if (request.UseSQLExecutor)
{
await Repository.UpdateAsync(e =>
{
e.Title = title;
e.Message = message;
e.TfaEnabled = request.TFAEnabled;
e.EnvelopeTypeId = settings.EnvelopeTypeId;
e.Language = settings.Language;
e.UseAccessCode = settings.UseAccessCode;
e.CertificationType = settings.CertificationType;
e.FinalEmailToCreator = settings.FinalEmailToCreator;
e.FinalEmailToReceivers = settings.FinalEmailToReceivers;
e.SendReminderEmails = settings.SendReminderEmails;
e.FirstReminderDays = settings.FirstReminderDays;
e.ReminderIntervalDays = settings.ReminderIntervalDays;
e.ExpiresWhenDays = settings.ExpiresWhenDays;
e.ExpiresWarningWhenDays = settings.ExpiresWarningWhenDays;
e.ExpiresWhen = settings.ExpiresWhen;
e.ExpiresWarningWhen = settings.ExpiresWarningWhen;
e.ContractType = settings.ContractType;
e.Status = EnvelopeStatus.EnvelopeSaved;
e.ChangedWhen = now;
}, e => e.Id == envelope.Id, cancel);
envelope.Title = title;
envelope.Message = message;
envelope.TfaEnabled = request.TFAEnabled;
envelope.EnvelopeTypeId = settings.EnvelopeTypeId;
envelope.Language = settings.Language;
envelope.UseAccessCode = settings.UseAccessCode;
envelope.CertificationType = settings.CertificationType;
envelope.FinalEmailToCreator = settings.FinalEmailToCreator;
envelope.FinalEmailToReceivers = settings.FinalEmailToReceivers;
envelope.SendReminderEmails = settings.SendReminderEmails;
envelope.FirstReminderDays = settings.FirstReminderDays;
envelope.ReminderIntervalDays = settings.ReminderIntervalDays;
envelope.ExpiresWhenDays = settings.ExpiresWhenDays;
envelope.ExpiresWarningWhenDays = settings.ExpiresWarningWhenDays;
envelope.ExpiresWhen = settings.ExpiresWhen;
envelope.ExpiresWarningWhen = settings.ExpiresWarningWhen;
envelope.ContractType = settings.ContractType;
envelope.Status = EnvelopeStatus.EnvelopeSaved;
envelope.ChangedWhen = now;
}
await Mediator.Send(new CreateHistoryCommand
{
EnvelopeId = envelope.Id,
UserReference = await ResolveSenderReferenceAsync(envelope, cancel),
Status = EnvelopeStatus.EnvelopeCreated
}, cancel);
}
await UpsertDocumentAsync(envelope.Id, request.Document, now, cancel);
await UpsertReceiversAndFieldsAsync(envelope.Id, request.Receivers, now, cancel);
if (request.Send)
await SendEnvelopeAsync(envelope, cancel);
envelope = await Repository.Query
.Include(e => e.User)
.Include(e => e.EnvelopeReceivers)
.ThenInclude(er => er.Receiver)
.FirstAsync(e => e.Id == envelope.Id, cancel);
return _mapper.Map<EnvelopeDto>(envelope); return _mapper.Map<EnvelopeDto>(envelope);
} }
}
private async Task<EnvelopeSettings> ResolveEnvelopeSettingsAsync(CreateEnvelopeCommand request, Envelope? existing, CancellationToken cancel)
{
EnvelopeType? selectedType = null;
if (request.EnvelopeTypeId is int envelopeTypeId && envelopeTypeId > 0)
{
selectedType = await EnvelopeTypeRepository.Query
.FirstOrDefaultAsync(t => t.Id == envelopeTypeId, cancel)
?? throw new BadRequestException($"Envelope type {envelopeTypeId} not found.");
}
var expiresWhenDays = Math.Max(0,
request.ExpiresWhenDays
?? selectedType?.ExpiresWhenDays
?? existing?.ExpiresWhenDays
?? 0);
var expiresWarningWhenDays = Math.Max(0,
request.ExpiresWarningWhenDays
?? selectedType?.ExpiresWarningWhenDays
?? existing?.ExpiresWarningWhenDays
?? 0);
var useAccessCode = request.UseAccessCode
?? selectedType?.UseAccessCode
?? existing?.UseAccessCode
?? false;
if (request.TFAEnabled)
useAccessCode = true;
return new EnvelopeSettings(
EnvelopeTypeId: request.EnvelopeTypeId ?? existing?.EnvelopeTypeId,
Language: ResolveLanguage(request.Language, selectedType?.Language, existing?.Language),
UseAccessCode: useAccessCode,
CertificationType: request.CertificationType
?? (int?)selectedType?.CertificationType
?? existing?.CertificationType
?? (int)CertificationType.AdvancedElectronicSignature,
FinalEmailToCreator: request.FinalEmailToCreator
?? (int?)selectedType?.FinalEmailToCreator
?? existing?.FinalEmailToCreator
?? (int)FinalEmailType.No,
FinalEmailToReceivers: request.FinalEmailToReceivers
?? (int?)selectedType?.FinalEmailToReceivers
?? existing?.FinalEmailToReceivers
?? (int)FinalEmailType.No,
SendReminderEmails: request.SendReminderEmails
?? selectedType?.SendReminderEmails
?? existing?.SendReminderEmails
?? false,
FirstReminderDays: Math.Max(0,
request.FirstReminderDays
?? selectedType?.FirstReminderDays
?? existing?.FirstReminderDays
?? 0),
ReminderIntervalDays: Math.Max(0,
request.ReminderIntervalDays
?? selectedType?.ReminderIntervalDays
?? existing?.ReminderIntervalDays
?? 0),
ExpiresWhenDays: expiresWhenDays,
ExpiresWarningWhenDays: expiresWarningWhenDays,
ExpiresWhen: expiresWhenDays > 0 ? DateTime.Now.AddDays(expiresWhenDays) : null,
ExpiresWarningWhen: expiresWarningWhenDays > 0 ? DateTime.Now.AddDays(expiresWarningWhenDays) : null,
ContractType: existing?.ContractType
?? selectedType?.ContractType
?? (int)ContractType.Contract);
}
private static string ResolveLanguage(string? preferred, string? fallback1, string? fallback2)
{
if (!string.IsNullOrWhiteSpace(preferred))
return preferred.Trim();
if (!string.IsNullOrWhiteSpace(fallback1))
return fallback1.Trim();
if (!string.IsNullOrWhiteSpace(fallback2))
return fallback2.Trim();
return "de-DE";
}
private sealed record EnvelopeSettings(
int? EnvelopeTypeId,
string Language,
bool UseAccessCode,
int CertificationType,
int FinalEmailToCreator,
int FinalEmailToReceivers,
bool SendReminderEmails,
int FirstReminderDays,
int ReminderIntervalDays,
int ExpiresWhenDays,
int ExpiresWarningWhenDays,
DateTime? ExpiresWhen,
DateTime? ExpiresWarningWhen,
int ContractType);
private async Task<string> ResolveSenderReferenceAsync(Envelope envelope, CancellationToken cancel)
{
if (!string.IsNullOrWhiteSpace(envelope.User?.Email))
return envelope.User.Email;
var senderEmail = await Repository.Query
.Where(e => e.Id == envelope.Id)
.Select(e => e.User.Email)
.FirstOrDefaultAsync(cancel);
if (string.IsNullOrWhiteSpace(senderEmail))
throw new BadRequestException("Envelope sender email is missing.");
return senderEmail;
}
private async Task UpsertDocumentAsync(int envelopeId, CreateEnvelopeDocumentDto? document, DateTime now, CancellationToken cancel)
{
if (document is null || string.IsNullOrWhiteSpace(document.DataAsBase64))
return;
byte[] bytes;
try
{
bytes = Convert.FromBase64String(document.DataAsBase64);
}
catch (FormatException)
{
throw new BadRequestException("Document payload is not valid base64.");
}
var existingDocumentId = await DocumentRepository.Query
.Where(d => d.EnvelopeId == envelopeId)
.Select(d => (int?)d.Id)
.FirstOrDefaultAsync(cancel);
if (existingDocumentId is null)
{
await DocumentRepository.CreateAsync(new Document
{
EnvelopeId = envelopeId,
AddedWhen = now,
ByteData = bytes,
Filename = string.Empty,
Filepath = string.Empty,
FileNameOriginal = string.Empty
}, cancel);
return;
}
await DocumentRepository.UpdateAsync(d =>
{
d.ByteData = bytes;
}, d => d.Id == existingDocumentId.Value, cancel);
}
private async Task UpsertReceiversAndFieldsAsync(int envelopeId, IReadOnlyCollection<CreateEnvelopeReceiverDto> receivers, DateTime now, CancellationToken cancel)
{
var filteredReceivers = receivers
.Where(r => !string.IsNullOrWhiteSpace(r.EmailAddress))
.Select(r => r with
{
EmailAddress = r.EmailAddress.Trim().ToLowerInvariant(),
Name = r.Name?.Trim(),
PhoneNumber = string.IsNullOrWhiteSpace(r.PhoneNumber) ? null : r.PhoneNumber.Trim(),
Fields = r.Fields ?? new List<CreateEnvelopeFieldDto>()
})
.GroupBy(r => r.EmailAddress, StringComparer.OrdinalIgnoreCase)
.Select(g => g.First())
.ToList();
if (filteredReceivers.Count == 0)
return;
var document = await DocumentRepository.Query
.FirstOrDefaultAsync(d => d.EnvelopeId == envelopeId, cancel);
if (document is null)
return;
var currentLinks = await EnvelopeReceiverRepository.Query
.Include(er => er.Receiver)
.Where(er => er.EnvelopeId == envelopeId)
.ToListAsync(cancel);
var linksByEmail = currentLinks
.Where(er => !string.IsNullOrWhiteSpace(er.Receiver?.EmailAddress))
.GroupBy(er => er.Receiver!.EmailAddress, StringComparer.OrdinalIgnoreCase)
.ToDictionary(g => g.Key.ToLowerInvariant(), g => g.First(), StringComparer.OrdinalIgnoreCase);
var sequence = currentLinks.Count > 0 ? currentLinks.Max(er => er.Sequence) : 0;
foreach (var receiverDraft in filteredReceivers)
{
var linkResult = await GetOrCreateEnvelopeReceiverAsync(envelopeId, receiverDraft, linksByEmail, now, sequence, cancel);
var link = linkResult.Link;
sequence = linkResult.NextSequence;
await DocReceiverElementRepository.DeleteAsync(query => query
.Where(e => e.DocumentId == document.Id)
.Where(e => e.ReceiverId == link.ReceiverId), cancel);
foreach (var field in receiverDraft.Fields.Where(f => f.Page > 0))
{
await DocReceiverElementRepository.CreateAsync(new DocReceiverElement
{
DocumentId = document.Id,
ReceiverId = link.ReceiverId,
ElementType = 1,
X = field.X,
Y = field.Y,
Width = 1.77,
Height = 1.96,
Page = field.Page,
Required = true,
ReadOnly = false,
AnnotationIndex = 0,
AddedWhen = now,
FullName = link.Name,
Position = string.Empty,
Place = string.Empty,
Tooltip = string.Empty
}, cancel);
}
}
}
private async Task<(EnvelopeReceiver Link, int NextSequence)> GetOrCreateEnvelopeReceiverAsync(
int envelopeId,
CreateEnvelopeReceiverDto receiverDraft,
IDictionary<string, EnvelopeReceiver> linksByEmail,
DateTime now,
int sequence,
CancellationToken cancel)
{
if (linksByEmail.TryGetValue(receiverDraft.EmailAddress, out var existingLink))
{
var nextName = string.IsNullOrWhiteSpace(receiverDraft.Name) ? existingLink.Name : receiverDraft.Name!;
var nextPhoneNumber = receiverDraft.PhoneNumber ?? existingLink.PhoneNumber;
await EnvelopeReceiverRepository.UpdateAsync(er =>
{
er.Name = nextName;
er.PhoneNumber = nextPhoneNumber;
er.ChangedWhen = now;
}, er => er.EnvelopeId == existingLink.EnvelopeId && er.ReceiverId == existingLink.ReceiverId, cancel);
existingLink.Name = nextName;
existingLink.PhoneNumber = nextPhoneNumber;
return (existingLink, sequence);
}
var receiver = await ReceiverRepository.Query
.FirstOrDefaultAsync(r => r.EmailAddress != null && r.EmailAddress.ToLower() == receiverDraft.EmailAddress, cancel);
if (receiver is null)
{
receiver = await ReceiverRepository.CreateAsync(new Receiver
{
EmailAddress = receiverDraft.EmailAddress,
Signature = receiverDraft.EmailAddress.ToUpperInvariant().GetChecksum(),
AddedWhen = now,
TotpSecretkey = string.Empty,
TfaRegDeadline = null
}, cancel);
}
sequence++;
var link = await EnvelopeReceiverRepository.CreateAsync(new EnvelopeReceiver
{
EnvelopeId = envelopeId,
ReceiverId = receiver.Id,
Sequence = sequence,
Name = receiverDraft.Name ?? receiverDraft.EmailAddress,
JobTitle = string.Empty,
CompanyName = string.Empty,
PrivateMessage = string.Empty,
AccessCode = string.Empty,
AddedWhen = now,
ChangedWhen = now,
PhoneNumber = receiverDraft.PhoneNumber ?? string.Empty
}, cancel);
link.Receiver = receiver;
linksByEmail[receiverDraft.EmailAddress] = link;
return (link, sequence);
}
private async Task SendEnvelopeAsync(Envelope envelope, CancellationToken cancel)
{
var hasDocument = await DocumentRepository.Query
.AnyAsync(d => d.EnvelopeId == envelope.Id && d.ByteData != null && d.ByteData.Length > 0, cancel);
if (!hasDocument)
throw new BadRequestException("Cannot send envelope without a document.");
var links = await EnvelopeReceiverRepository.Query
.Include(er => er.Receiver)
.Where(er => er.EnvelopeId == envelope.Id)
.ToListAsync(cancel);
if (links.Count == 0)
throw new BadRequestException("Cannot send envelope without receivers.");
var receiverIds = links.Select(l => l.ReceiverId).Distinct().ToList();
var documentIds = await DocumentRepository.Query
.Where(d => d.EnvelopeId == envelope.Id)
.Select(d => d.Id)
.ToListAsync(cancel);
var receiverFieldCounts = await DocReceiverElementRepository.Query
.Where(e => documentIds.Contains(e.DocumentId))
.Where(e => receiverIds.Contains(e.ReceiverId))
.GroupBy(e => e.ReceiverId)
.Select(g => new { ReceiverId = g.Key, Count = g.Count() })
.ToDictionaryAsync(x => x.ReceiverId, x => x.Count, cancel);
var receiversWithoutFields = links
.Where(link => !receiverFieldCounts.TryGetValue(link.ReceiverId, out var count) || count <= 0)
.Select(link => link.Name)
.ToList();
if (receiversWithoutFields.Count > 0)
throw new BadRequestException("Cannot send envelope because some receivers have no signature fields.");
var changedWhen = DateTime.Now;
await Repository.UpdateAsync(e =>
{
e.Status = EnvelopeStatus.EnvelopeQueued;
e.ChangedWhen = changedWhen;
}, e => e.Id == envelope.Id, cancel);
var userReference = await ResolveSenderReferenceAsync(envelope, cancel);
await Mediator.Send(new CreateHistoryCommand
{
EnvelopeId = envelope.Id,
UserReference = userReference,
Status = EnvelopeStatus.EnvelopeQueued
}, cancel);
await QueueInvitationEmailsAsync(envelope, links, userReference, cancel);
envelope.Status = EnvelopeStatus.EnvelopeQueued;
}
private async Task QueueInvitationEmailsAsync(
Envelope envelope,
IReadOnlyCollection<EnvelopeReceiver> links,
string senderReference,
CancellationToken cancel)
{
var template = await EmailTemplateRepository.Query
.SingleOrDefaultAsync(x => x.Name == EmailTemplateType.DocumentReceived.ToString(), cancel)
?? throw new InvalidOperationException($"Email template not found: {EmailTemplateType.DocumentReceived}");
var signatureHost = await ConfigService.ReadDefaultSignatureHost();
var senderName = senderReference;
foreach (var link in links)
{
var receiver = link.Receiver;
if (receiver is null || string.IsNullOrWhiteSpace(receiver.EmailAddress) || string.IsNullOrWhiteSpace(receiver.Signature))
continue;
var envelopeKey = (envelope.Uuid, receiver.Signature).ToEnvelopeKey();
var documentLink = $"{signatureHost.TrimEnd('/')}/envelope/login/{Uri.EscapeDataString(envelopeKey)}";
var placeholders = new Dictionary<string, string>(MailParams.Placeholders)
{
["[NAME_RECEIVER]"] = link.Name ?? string.Empty,
["[NAME_SENDER]"] = senderName ?? string.Empty,
["[DOCUMENT_TITLE]"] = envelope.Title ?? string.Empty,
["[MESSAGE]"] = envelope.Message ?? string.Empty,
["[LINK_TO_DOCUMENT]"] = documentLink,
["[LINK_TO_DOCUMENT_TEXT]"] = documentLink[..Math.Min(40, documentLink.Length)] + ".."
};
var subject = ReplacePlaceHolders(template.Subject, placeholders);
var body = ReplacePlaceHolders(template.Body, placeholders);
var emailOut = new EmailOut
{
EmailAddress = receiver.EmailAddress,
EmailSubj = subject,
EmailBody = body,
AddedWhen = DateTime.Now,
AddedWho = DispatcherParams.AddedWho,
SendingProfile = DispatcherParams.SendingProfile,
ReminderTypeId = DispatcherParams.ReminderTypeId,
EmailAttmt1 = DispatcherParams.EmailAttmt1,
WfId = (int)EnvelopeStatus.MessageInvitationSent,
ReferenceString = receiver.EmailAddress,
ReferenceId = link.ReceiverId
};
await EmailOutRepository.CreateAsync(emailOut, cancel);
}
}
private static string ReplacePlaceHolders(string text, Dictionary<string, string> placeHolders)
{
foreach (var placeHolder in placeHolders)
text = text.Replace(placeHolder.Key, placeHolder.Value);
return text;
}
}

View File

@@ -0,0 +1,25 @@
using System.ComponentModel.DataAnnotations;
using EnvelopeGenerator.Domain.Constants;
using MediatR;
namespace EnvelopeGenerator.Application.Envelopes.Commands;
public record DeleteEnvelopeResultDto(int EnvelopeId, EnvelopeStatus AppliedStatus, bool IsNoOp);
public record DeleteEnvelopeCommand : IRequest<DeleteEnvelopeResultDto>
{
[Required]
public int EnvelopeId { get; init; }
[Required]
[MinLength(1)]
public string Reason { get; init; } = string.Empty;
internal int UserId { get; private set; }
public DeleteEnvelopeCommand WithAuth(int userId)
{
UserId = userId;
return this;
}
}

View File

@@ -0,0 +1,89 @@
using EnvelopeGenerator.Application.Common.Notifications.EnvelopeDeleted;
using DigitalData.Core.Abstraction.Application.Repository;
using DigitalData.Core.Exceptions;
using EnvelopeGenerator.Application.Histories.Commands;
using EnvelopeGenerator.Domain.Constants;
using EnvelopeGenerator.Domain.Entities;
using MediatR;
using Microsoft.EntityFrameworkCore;
using System.Linq;
namespace EnvelopeGenerator.Application.Envelopes.Commands;
public class DeleteEnvelopeCommandHandler : IRequestHandler<DeleteEnvelopeCommand, DeleteEnvelopeResultDto>
{
private readonly IRepository<Envelope> _envelopeRepository;
private readonly IMediator _mediator;
public DeleteEnvelopeCommandHandler(
IRepository<Envelope> envelopeRepository,
IMediator mediator)
{
_envelopeRepository = envelopeRepository;
_mediator = mediator;
}
public async Task<DeleteEnvelopeResultDto> Handle(DeleteEnvelopeCommand request, CancellationToken cancel)
{
var reason = request.Reason?.Trim();
if (string.IsNullOrWhiteSpace(reason))
throw new BadRequestException("Please provide a reason.");
var envelope = await _envelopeRepository.Query
.Include(e => e.User)
.Include(e => e.EnvelopeReceivers)
.ThenInclude(er => er.Receiver)
.FirstOrDefaultAsync(e => e.Id == request.EnvelopeId, cancel)
?? throw new NotFoundException($"Envelope {request.EnvelopeId} not found.");
if (envelope.UserId != request.UserId)
throw new ForbiddenException();
if (envelope.Status is EnvelopeStatus.EnvelopeDeleted or EnvelopeStatus.EnvelopeWithdrawn)
return new DeleteEnvelopeResultDto(envelope.Id, envelope.Status, IsNoOp: true);
var targetStatus = envelope.Status > EnvelopeStatus.EnvelopeSaved
? EnvelopeStatus.EnvelopeWithdrawn
: EnvelopeStatus.EnvelopeDeleted;
var changedWhen = DateTime.Now;
envelope.Status = targetStatus;
envelope.Comment = reason;
envelope.ChangedWhen = changedWhen;
await _envelopeRepository.UpdateAsync(e =>
{
e.Status = targetStatus;
e.Comment = reason;
e.ChangedWhen = changedWhen;
}, e => e.Id == envelope.Id, cancel);
var userReference = envelope.User?.Email;
if (string.IsNullOrWhiteSpace(userReference))
throw new BadRequestException("Envelope sender email is missing.");
await _mediator.Send(new CreateHistoryCommand
{
EnvelopeId = envelope.Id,
UserReference = userReference,
Status = targetStatus,
Comment = reason
}, cancel);
foreach (var envelopeReceiver in envelope.EnvelopeReceivers ?? Enumerable.Empty<EnvelopeReceiver>())
{
if (envelopeReceiver.Receiver is null)
continue;
await _mediator.Publish(new EnvelopeDeletedNotification
{
Envelope = envelope,
EnvelopeReceiver = envelopeReceiver,
Reason = reason,
SenderReference = userReference
}, cancel);
}
return new DeleteEnvelopeResultDto(envelope.Id, targetStatus, IsNoOp: false);
}
}

View File

@@ -14,6 +14,16 @@ namespace EnvelopeGenerator.Application.Envelopes.Queries;
/// </summary> /// </summary>
public record ReadEnvelopeQuery : EnvelopeQueryBase, IRequest<IEnumerable<EnvelopeDto>> public record ReadEnvelopeQuery : EnvelopeQueryBase, IRequest<IEnumerable<EnvelopeDto>>
{ {
/// <summary>
///
/// </summary>
public bool OnlyActive { get; init; } = false;
/// <summary>
///
/// </summary>
public bool OnlyCompleted { get; init; } = false;
/// <summary> /// <summary>
/// Abfrage des Include des Umschlags /// Abfrage des Include des Umschlags
/// </summary> /// </summary>
@@ -22,7 +32,7 @@ public record ReadEnvelopeQuery : EnvelopeQueryBase, IRequest<IEnumerable<Envelo
/// <summary> /// <summary>
/// Optionaler Benutzerfilter; wenn gesetzt, werden nur Umschläge des Benutzers geladen. /// Optionaler Benutzerfilter; wenn gesetzt, werden nur Umschläge des Benutzers geladen.
/// </summary> /// </summary>
public int? UserId { get; init; } internal int? UserId { get; init; }
/// <summary> /// <summary>
/// Setzt den Benutzerkontext für die Abfrage. /// Setzt den Benutzerkontext für die Abfrage.
@@ -132,10 +142,18 @@ 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.Documents) .Include(e => e.Type)
.Include(e => e.EnvelopeReceivers).ThenInclude(er => er.Receiver)
.Include(e => e.Histories)
.ToListAsync(cancel); .ToListAsync(cancel);
return _mapper.Map<IEnumerable<EnvelopeDto>>(envelopes); return _mapper.Map<IEnumerable<EnvelopeDto>>(envelopes);
} }
} }

View File

@@ -3,7 +3,6 @@ using DigitalData.Core.Abstraction.Application.Repository;
using EnvelopeGenerator.Application.Common.Dto.Receiver; using EnvelopeGenerator.Application.Common.Dto.Receiver;
using EnvelopeGenerator.Domain.Entities; using EnvelopeGenerator.Domain.Entities;
using MediatR; using MediatR;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
using System.ComponentModel.DataAnnotations; using System.ComponentModel.DataAnnotations;
using System.Security.Cryptography; using System.Security.Cryptography;
@@ -14,7 +13,6 @@ namespace EnvelopeGenerator.Application.Receivers.Commands;
/// <summary> /// <summary>
/// ///
/// </summary> /// </summary>
[ApiExplorerSettings(IgnoreApi = true)]
public record CreateReceiverCommand : IRequest<(ReceiverDto Receiver, bool AlreadyExists)> public record CreateReceiverCommand : IRequest<(ReceiverDto Receiver, bool AlreadyExists)>
{ {
/// <summary> /// <summary>

View File

@@ -1,11 +1,8 @@
using Microsoft.AspNetCore.Mvc; namespace EnvelopeGenerator.Application.Receivers.Commands;
namespace EnvelopeGenerator.Application.Receivers.Commands;
/// <summary> /// <summary>
/// Data Transfer Object for updating a receiver's information. /// Data Transfer Object for updating a receiver's information.
/// </summary> /// </summary>
[ApiExplorerSettings(IgnoreApi = true)]
public class UpdateReceiverCommand public class UpdateReceiverCommand
{ {
/// <summary> /// <summary>

View File

@@ -5,6 +5,7 @@ using EnvelopeGenerator.Application.Common.Query;
using MediatR; using MediatR;
using EnvelopeGenerator.Domain.Entities; using EnvelopeGenerator.Domain.Entities;
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
using System.ComponentModel.DataAnnotations.Schema;
namespace EnvelopeGenerator.Application.Receivers.Queries; namespace EnvelopeGenerator.Application.Receivers.Queries;
@@ -12,7 +13,24 @@ namespace EnvelopeGenerator.Application.Receivers.Queries;
/// Stellt eine Abfrage dar, um die Details eines Empfängers zu lesen. /// Stellt eine Abfrage dar, um die Details eines Empfängers zu lesen.
/// um spezifische Informationen über einen Empfänger abzurufen. /// um spezifische Informationen über einen Empfänger abzurufen.
/// </summary> /// </summary>
public record ReadReceiverQuery : ReceiverQueryBase, IRequest<IEnumerable<ReceiverDto>>; public record ReadReceiverQuery : ReceiverQueryBase, IRequest<IEnumerable<ReceiverDto>>
{
/// <summary>
/// Suchbegriff für eine teilweise Übereinstimmung in der E-Mail Adresse des Empfängers
/// </summary>
public virtual string? EmailAddressSearch { get; set; }
/// <summary>
/// Checks whether any of the specified query criteria have a value.
/// </summary>
/// <remarks>
/// This property returns <c>true</c> if at least one of the fields
/// <see cref="ReceiverQueryBase.Id"/>, <see cref="ReceiverQueryBase.EmailAddress"/>, <see cref="EmailAddressSearch"/>, or <see cref="ReceiverQueryBase.Signature"/> is not null.
/// <para>Usage example: The query can be executed only if at least one criterion is specified.</para>
/// </remarks>
[NotMapped]
public override bool HasAnyCriteria => EmailAddressSearch is not null || base.HasAnyCriteria;
}
/// <summary> /// <summary>
/// ///
@@ -53,6 +71,11 @@ public class ReadReceiverQueryHandler : IRequestHandler<ReadReceiverQuery, IEnum
query = query.Where(r => r.EmailAddress == email); query = query.Where(r => r.EmailAddress == email);
} }
if (!string.IsNullOrWhiteSpace(request.EmailAddressSearch))
{
query = query.Where(r => EF.Functions.Like(r.EmailAddress, $"%{request.EmailAddressSearch}%"));
}
if (request.Signature is string signature) if (request.Signature is string signature)
{ {
query = query.Where(r => r.Signature == signature); query = query.Where(r => r.Signature == signature);

View File

@@ -397,4 +397,412 @@ public static class Extensions
/// <param name="suffix"></param> /// <param name="suffix"></param>
/// <returns></returns> /// <returns></returns>
public static string LockedFooterBody(this IStringLocalizer localizer, string suffix) => localizer[nameof(LockedFooterBody) + suffix].Value; public static string LockedFooterBody(this IStringLocalizer localizer, string suffix) => localizer[nameof(LockedFooterBody) + suffix].Value;
// Sender-side UI resources
/// <summary>
///
/// </summary>
/// <param name="localizer"></param>
/// <returns></returns>
public static string NewEnvelope(this IStringLocalizer localizer) => localizer[nameof(NewEnvelope)].Value;
/// <summary>
///
/// </summary>
/// <param name="localizer"></param>
/// <returns></returns>
public static string LoadEnvelope(this IStringLocalizer localizer) => localizer[nameof(LoadEnvelope)].Value;
/// <summary>
///
/// </summary>
/// <param name="localizer"></param>
/// <returns></returns>
public static string DeleteEnvelope(this IStringLocalizer localizer) => localizer[nameof(DeleteEnvelope)].Value;
/// <summary>
///
/// </summary>
/// <param name="localizer"></param>
/// <returns></returns>
public static string RefreshData(this IStringLocalizer localizer) => localizer[nameof(RefreshData)].Value;
/// <summary>
///
/// </summary>
/// <param name="localizer"></param>
/// <returns></returns>
public static string RefreshedAt(this IStringLocalizer localizer) => localizer[nameof(RefreshedAt)].Value;
/// <summary>
///
/// </summary>
/// <param name="localizer"></param>
/// <returns></returns>
public static string ShowDocument(this IStringLocalizer localizer) => localizer[nameof(ShowDocument)].Value;
/// <summary>
///
/// </summary>
/// <param name="localizer"></param>
/// <returns></returns>
public static string ContactReceiver(this IStringLocalizer localizer) => localizer[nameof(ContactReceiver)].Value;
/// <summary>
///
/// </summary>
/// <param name="localizer"></param>
/// <returns></returns>
public static string EnvelopeId(this IStringLocalizer localizer) => localizer[nameof(EnvelopeId)].Value;
/// <summary>
///
/// </summary>
/// <param name="localizer"></param>
/// <returns></returns>
public static string OpenLogDirectory(this IStringLocalizer localizer) => localizer[nameof(OpenLogDirectory)].Value;
/// <summary>
///
/// </summary>
/// <param name="localizer"></param>
/// <returns></returns>
public static string ShowResultsReport(this IStringLocalizer localizer) => localizer[nameof(ShowResultsReport)].Value;
/// <summary>
///
/// </summary>
/// <param name="localizer"></param>
/// <returns></returns>
public static string SupportMail(this IStringLocalizer localizer) => localizer[nameof(SupportMail)].Value;
/// <summary>
///
/// </summary>
/// <param name="localizer"></param>
/// <returns></returns>
public static string ResendInvitation(this IStringLocalizer localizer) => localizer[nameof(ResendInvitation)].Value;
/// <summary>
///
/// </summary>
/// <param name="localizer"></param>
/// <returns></returns>
public static string Export(this IStringLocalizer localizer) => localizer[nameof(Export)].Value;
/// <summary>
///
/// </summary>
/// <param name="localizer"></param>
/// <returns></returns>
public static string Receivers(this IStringLocalizer localizer) => localizer[nameof(Receivers)].Value;
/// <summary>
///
/// </summary>
/// <param name="localizer"></param>
/// <returns></returns>
public static string EmailSalutation(this IStringLocalizer localizer) => localizer[nameof(EmailSalutation)].Value;
/// <summary>
///
/// </summary>
/// <param name="localizer"></param>
/// <returns></returns>
public static string SignedWhen(this IStringLocalizer localizer) => localizer[nameof(SignedWhen)].Value;
/// <summary>
///
/// </summary>
/// <param name="localizer"></param>
/// <returns></returns>
public static string AccessCode(this IStringLocalizer localizer) => localizer[nameof(AccessCode)].Value;
/// <summary>
///
/// </summary>
/// <param name="localizer"></param>
/// <returns></returns>
public static string User(this IStringLocalizer localizer) => localizer[nameof(User)].Value;
/// <summary>
///
/// </summary>
/// <param name="localizer"></param>
/// <returns></returns>
public static string Type(this IStringLocalizer localizer) => localizer[nameof(Type)].Value;
/// <summary>
///
/// </summary>
/// <param name="localizer"></param>
/// <returns></returns>
public static string Title(this IStringLocalizer localizer) => localizer[nameof(Title)].Value;
/// <summary>
///
/// </summary>
/// <param name="localizer"></param>
/// <returns></returns>
public static string CreatedOn(this IStringLocalizer localizer) => localizer[nameof(CreatedOn)].Value;
/// <summary>
///
/// </summary>
/// <param name="localizer"></param>
/// <returns></returns>
public static string LastModified(this IStringLocalizer localizer) => localizer[nameof(LastModified)].Value;
/// <summary>
///
/// </summary>
/// <param name="localizer"></param>
/// <returns></returns>
public static string OpenEnvelopes(this IStringLocalizer localizer) => localizer[nameof(OpenEnvelopes)].Value;
/// <summary>
///
/// </summary>
/// <param name="localizer"></param>
/// <returns></returns>
public static string CompletedEnvelopes(this IStringLocalizer localizer) => localizer[nameof(CompletedEnvelopes)].Value;
/// <summary>
///
/// </summary>
/// <param name="localizer"></param>
/// <returns></returns>
public static string SendAccessCode(this IStringLocalizer localizer) => localizer[nameof(SendAccessCode)].Value;
/// <summary>
///
/// </summary>
/// <param name="localizer"></param>
/// <returns></returns>
public static string TwoFactorProperties(this IStringLocalizer localizer) => localizer[nameof(TwoFactorProperties)].Value;
/// <summary>
///
/// </summary>
/// <param name="localizer"></param>
/// <returns></returns>
public static string Name(this IStringLocalizer localizer) => localizer[nameof(Name)].Value;
/// <summary>
///
/// </summary>
/// <param name="localizer"></param>
/// <returns></returns>
public static string PhoneNumber(this IStringLocalizer localizer) => localizer[nameof(PhoneNumber)].Value;
/// <summary>
///
/// </summary>
/// <param name="localizer"></param>
/// <returns></returns>
public static string AddReceiver(this IStringLocalizer localizer) => localizer[nameof(AddReceiver)].Value;
/// <summary>
///
/// </summary>
/// <param name="localizer"></param>
/// <returns></returns>
public static string DeleteReceiver(this IStringLocalizer localizer) => localizer[nameof(DeleteReceiver)].Value;
/// <summary>
///
/// </summary>
/// <param name="localizer"></param>
/// <returns></returns>
public static string AddFile(this IStringLocalizer localizer) => localizer[nameof(AddFile)].Value;
/// <summary>
///
/// </summary>
/// <param name="localizer"></param>
/// <returns></returns>
public static string MergeFiles(this IStringLocalizer localizer) => localizer[nameof(MergeFiles)].Value;
/// <summary>
///
/// </summary>
/// <param name="localizer"></param>
/// <returns></returns>
public static string DeleteFile(this IStringLocalizer localizer) => localizer[nameof(DeleteFile)].Value;
/// <summary>
///
/// </summary>
/// <param name="localizer"></param>
/// <returns></returns>
public static string ShowFile(this IStringLocalizer localizer) => localizer[nameof(ShowFile)].Value;
/// <summary>
///
/// </summary>
/// <param name="localizer"></param>
/// <returns></returns>
public static string EditFields(this IStringLocalizer localizer) => localizer[nameof(EditFields)].Value;
/// <summary>
///
/// </summary>
/// <param name="localizer"></param>
/// <returns></returns>
public static string EditData(this IStringLocalizer localizer) => localizer[nameof(EditData)].Value;
/// <summary>
///
/// </summary>
/// <param name="localizer"></param>
/// <returns></returns>
public static string Save(this IStringLocalizer localizer) => localizer[nameof(Save)].Value;
/// <summary>
///
/// </summary>
/// <param name="localizer"></param>
/// <returns></returns>
public static string SendEnvelope(this IStringLocalizer localizer) => localizer[nameof(SendEnvelope)].Value;
/// <summary>
///
/// </summary>
/// <param name="localizer"></param>
/// <returns></returns>
public static string Cancel(this IStringLocalizer localizer) => localizer[nameof(Cancel)].Value;
/// <summary>
///
/// </summary>
/// <param name="localizer"></param>
/// <returns></returns>
public static string AddSignature(this IStringLocalizer localizer) => localizer[nameof(AddSignature)].Value;
/// <summary>
///
/// </summary>
/// <param name="localizer"></param>
/// <returns></returns>
public static string DeleteSignature(this IStringLocalizer localizer) => localizer[nameof(DeleteSignature)].Value;
/// <summary>
///
/// </summary>
/// <param name="localizer"></param>
/// <returns></returns>
public static string Language(this IStringLocalizer localizer) => localizer[nameof(Language)].Value;
/// <summary>
///
/// </summary>
/// <param name="localizer"></param>
/// <returns></returns>
public static string UseAccessCode(this IStringLocalizer localizer) => localizer[nameof(UseAccessCode)].Value;
/// <summary>
///
/// </summary>
/// <param name="localizer"></param>
/// <returns></returns>
public static string TwoFactorEnabled(this IStringLocalizer localizer) => localizer[nameof(TwoFactorEnabled)].Value;
/// <summary>
///
/// </summary>
/// <param name="localizer"></param>
/// <returns></returns>
public static string CertificationType(this IStringLocalizer localizer) => localizer[nameof(CertificationType)].Value;
/// <summary>
///
/// </summary>
/// <param name="localizer"></param>
/// <returns></returns>
public static string FinalEmailToCreator(this IStringLocalizer localizer) => localizer[nameof(FinalEmailToCreator)].Value;
/// <summary>
///
/// </summary>
/// <param name="localizer"></param>
/// <returns></returns>
public static string FinalEmailToReceivers(this IStringLocalizer localizer) => localizer[nameof(FinalEmailToReceivers)].Value;
/// <summary>
///
/// </summary>
/// <param name="localizer"></param>
/// <returns></returns>
public static string SendReminderEmails(this IStringLocalizer localizer) => localizer[nameof(SendReminderEmails)].Value;
/// <summary>
///
/// </summary>
/// <param name="localizer"></param>
/// <returns></returns>
public static string FirstReminderDays(this IStringLocalizer localizer) => localizer[nameof(FirstReminderDays)].Value;
/// <summary>
///
/// </summary>
/// <param name="localizer"></param>
/// <returns></returns>
public static string ReminderIntervalDays(this IStringLocalizer localizer) => localizer[nameof(ReminderIntervalDays)].Value;
/// <summary>
///
/// </summary>
/// <param name="localizer"></param>
/// <returns></returns>
public static string ExpiresWhenDays(this IStringLocalizer localizer) => localizer[nameof(ExpiresWhenDays)].Value;
/// <summary>
///
/// </summary>
/// <param name="localizer"></param>
/// <returns></returns>
public static string ExpiresWarningDays(this IStringLocalizer localizer) => localizer[nameof(ExpiresWarningDays)].Value;
/// <summary>
///
/// </summary>
/// <param name="localizer"></param>
/// <returns></returns>
public static string Message(this IStringLocalizer localizer) => localizer[nameof(Message)].Value;
/// <summary>
///
/// </summary>
/// <param name="localizer"></param>
/// <returns></returns>
public static string EnvelopeType(this IStringLocalizer localizer) => localizer[nameof(EnvelopeType)].Value;
/// <summary>
///
/// </summary>
/// <param name="localizer"></param>
/// <returns></returns>
public static string AllOptions(this IStringLocalizer localizer) => localizer[nameof(AllOptions)].Value;
/// <summary>
///
/// </summary>
/// <param name="localizer"></param>
/// <returns></returns>
public static string DeleteReason(this IStringLocalizer localizer) => localizer[nameof(DeleteReason)].Value;
/// <summary>
///
/// </summary>
/// <param name="localizer"></param>
/// <returns></returns>
public static string PleaseProvideReason(this IStringLocalizer localizer) => localizer[nameof(PleaseProvideReason)].Value;
/// <summary>
///
/// </summary>
/// <param name="localizer"></param>
/// <returns></returns>
public static string Status(this IStringLocalizer localizer) => localizer[nameof(Status)].Value;
} }

View File

@@ -477,4 +477,299 @@
<data name="Confirmations" xml:space="preserve"> <data name="Confirmations" xml:space="preserve">
<value>Bestätigungen</value> <value>Bestätigungen</value>
</data> </data>
</root> <data name="NewEnvelope" xml:space="preserve">
<value>Neuer Umschlag</value>
</data>
<data name="LoadEnvelope" xml:space="preserve">
<value>Umschlag laden</value>
</data>
<data name="DeleteEnvelope" xml:space="preserve">
<value>Umschlag zurückrufen/löschen</value>
</data>
<data name="RefreshData" xml:space="preserve">
<value>Daten Aktualisieren</value>
</data>
<data name="RefreshedAt" xml:space="preserve">
<value>Aktualisiert: {0}</value>
</data>
<data name="ShowDocument" xml:space="preserve">
<value>Dokument anzeigen</value>
</data>
<data name="ContactReceiver" xml:space="preserve">
<value>Empfänger kontaktieren</value>
</data>
<data name="EnvelopeId" xml:space="preserve">
<value>Umschlag-ID: {0}</value>
</data>
<data name="OpenLogDirectory" xml:space="preserve">
<value>Öffne Log Verzeichnis</value>
</data>
<data name="ShowResultsReport" xml:space="preserve">
<value>Ergebnisbericht anzeigen</value>
</data>
<data name="SupportMail" xml:space="preserve">
<value>Support Mail</value>
</data>
<data name="ResendInvitation" xml:space="preserve">
<value>Einladung manuell versenden</value>
</data>
<data name="Export" xml:space="preserve">
<value>Export</value>
</data>
<data name="Receivers" xml:space="preserve">
<value>Empfänger</value>
</data>
<data name="EmailSalutation" xml:space="preserve">
<value>Email Anrede</value>
</data>
<data name="SignedWhen" xml:space="preserve">
<value>Unterschrieben wann</value>
</data>
<data name="AccessCode" xml:space="preserve">
<value>Zugangscode</value>
</data>
<data name="User" xml:space="preserve">
<value>Benutzer</value>
</data>
<data name="Type" xml:space="preserve">
<value>Typ</value>
</data>
<data name="Title" xml:space="preserve">
<value>Titel</value>
</data>
<data name="CreatedOn" xml:space="preserve">
<value>Erstellt am</value>
</data>
<data name="LastModified" xml:space="preserve">
<value>Zuletzt geändert am</value>
</data>
<data name="OpenEnvelopes" xml:space="preserve">
<value>Offene Umschläge</value>
</data>
<data name="CompletedEnvelopes" xml:space="preserve">
<value>Abgeschlossene Umschläge</value>
</data>
<data name="SendAccessCode" xml:space="preserve">
<value>Zugangscode senden</value>
</data>
<data name="TwoFactorProperties" xml:space="preserve">
<value>2-Faktor Eigenschaften</value>
</data>
<data name="Name" xml:space="preserve">
<value>Name</value>
</data>
<data name="PhoneNumber" xml:space="preserve">
<value>Telefonnummer</value>
</data>
<data name="AddReceiver" xml:space="preserve">
<value>Empfänger hinzufügen</value>
</data>
<data name="DeleteReceiver" xml:space="preserve">
<value>Empfänger löschen</value>
</data>
<data name="AddFile" xml:space="preserve">
<value>Datei hinzufügen</value>
</data>
<data name="MergeFiles" xml:space="preserve">
<value>Dateien zusammenführen</value>
</data>
<data name="DeleteFile" xml:space="preserve">
<value>Datei löschen</value>
</data>
<data name="ShowFile" xml:space="preserve">
<value>Datei anzeigen</value>
</data>
<data name="EditFields" xml:space="preserve">
<value>Felder bearbeiten</value>
</data>
<data name="EditData" xml:space="preserve">
<value>Daten bearbeiten</value>
</data>
<data name="Save" xml:space="preserve">
<value>Speichern</value>
</data>
<data name="SendEnvelope" xml:space="preserve">
<value>Umschlag versenden</value>
</data>
<data name="Cancel" xml:space="preserve">
<value>Abbrechen</value>
</data>
<data name="AddSignature" xml:space="preserve">
<value>Signatur hinzufügen</value>
</data>
<data name="DeleteSignature" xml:space="preserve">
<value>Signatur löschen</value>
</data>
<data name="Language" xml:space="preserve">
<value>Sprache</value>
</data>
<data name="UseAccessCode" xml:space="preserve">
<value>Zugangscode verwenden</value>
</data>
<data name="TwoFactorEnabled" xml:space="preserve">
<value>2-Faktor-Authentifizierung aktiviert</value>
</data>
<data name="CertificationType" xml:space="preserve">
<value>Zertifizierungstyp</value>
</data>
<data name="FinalEmailToCreator" xml:space="preserve">
<value>Finale E-Mail an Ersteller</value>
</data>
<data name="FinalEmailToReceivers" xml:space="preserve">
<value>Finale E-Mail an Empfänger</value>
</data>
<data name="SendReminderEmails" xml:space="preserve">
<value>Erinnerungs-E-Mails senden</value>
</data>
<data name="FirstReminderDays" xml:space="preserve">
<value>Erste Erinnerung (Tage)</value>
</data>
<data name="ReminderIntervalDays" xml:space="preserve">
<value>Erinnerungsintervall (Tage)</value>
</data>
<data name="ExpiresWhenDays" xml:space="preserve">
<value>Läuft ab nach (Tage)</value>
</data>
<data name="ExpiresWarningDays" xml:space="preserve">
<value>Ablaufwarnung (Tage)</value>
</data>
<data name="Message" xml:space="preserve">
<value>Nachricht</value>
</data>
<data name="EnvelopeType" xml:space="preserve">
<value>Umschlagtyp</value>
</data>
<data name="AllOptions" xml:space="preserve">
<value>Alle Optionen</value>
</data>
<data name="DeleteReason" xml:space="preserve">
<value>Grund für Löschung</value>
</data>
<data name="PleaseProvideReason" xml:space="preserve">
<value>Bitte geben Sie einen Grund an</value>
</data>
<data name="Status" xml:space="preserve">
<value>Status</value>
</data>
<data name="ChangedAt" xml:space="preserve"><value>Geändert am</value></data>
<data name="Close" xml:space="preserve"><value>Schließen</value></data>
<data name="CloseLanguageMenu" xml:space="preserve"><value>Sprachmenü schließen</value></data>
<data name="ConfirmSignature" xml:space="preserve"><value>Unterschrift bestätigen</value></data>
<data name="Created" xml:space="preserve"><value>Erstellt</value></data>
<data name="Delete" xml:space="preserve"><value>Löschen</value></data>
<data name="Document" xml:space="preserve"><value>Dokument</value></data>
<data name="DocumentCouldNotBeLoaded" xml:space="preserve"><value>Dokument konnte nicht geladen werden</value></data>
<data name="DocumentLoading" xml:space="preserve"><value>Dokument wird geladen...</value></data>
<data name="EmailAddress" xml:space="preserve"><value>E-Mail-Adresse</value></data>
<data name="Envelope" xml:space="preserve"><value>Umschlag</value></data>
<data name="Error" xml:space="preserve"><value>Fehler</value></data>
<data name="ErrorLoadingDocument_WithReason" xml:space="preserve"><value>Fehler beim Laden des Dokuments: {0}</value></data>
<data name="ErrorPage_DevelopmentMode" xml:space="preserve"><value>Entwicklungsmodus</value></data>
<data name="ErrorPage_DevelopmentMode_Detail" xml:space="preserve"><value>Dies kann dazu führen, dass vertrauliche Informationen aus Ausnahmen Endbenutzern angezeigt werden. Für lokales Debugging setzen Sie ASPNETCORE_ENVIRONMENT auf Development und starten die App neu.</value></data>
<data name="ErrorPage_DevelopmentMode_Info" xml:space="preserve"><value>Im Entwicklungsmodus werden detailliertere Fehlerinformationen angezeigt.</value></data>
<data name="ErrorPage_DevelopmentMode_Warning" xml:space="preserve"><value>Der Entwicklungsmodus sollte in bereitgestellten Anwendungen nicht aktiviert sein.</value></data>
<data name="ErrorPage_ProcessingRequest" xml:space="preserve"><value>Bei der Verarbeitung Ihrer Anfrage ist ein Fehler aufgetreten.</value></data>
<data name="ErrorPage_RequestId" xml:space="preserve"><value>Anfrage-ID</value></data>
<data name="ErrorWithReason" xml:space="preserve"><value>Fehler: {0}</value></data>
<data name="From" xml:space="preserve"><value>Von</value></data>
<data name="FullName" xml:space="preserve"><value>Vor- und Nachname</value></data>
<data name="History" xml:space="preserve"><value>Historie</value></data>
<data name="Index_DigitalSignature" xml:space="preserve"><value>Digitale Unterschrift</value></data>
<data name="Index_PdfExport" xml:space="preserve"><value>PDF-Export</value></data>
<data name="Index_SecureAccess" xml:space="preserve"><value>Sicherer Zugang</value></data>
<data name="Index_Subtitle" xml:space="preserve"><value>Willkommen im eSign-Portal</value></data>
<data name="Index_ToEnvelopeOverview" xml:space="preserve"><value>Zur Umschlag-Übersicht</value></data>
<data name="Languages" xml:space="preserve"><value>Sprachen</value></data>
<data name="Loading" xml:space="preserve"><value>Lädt...</value></data>
<data name="Login" xml:space="preserve"><value>Anmelden</value></data>
<data name="LoginInProgress" xml:space="preserve"><value>Anmelden ...</value></data>
<data name="NotAuthenticatedPleaseLoginAgain" xml:space="preserve"><value>Nicht authentifiziert. Bitte melden Sie sich erneut an.</value></data>
<data name="Only" xml:space="preserve"><value>Nur</value></data>
<data name="OpenDocument" xml:space="preserve"><value>Dokument öffnen</value></data>
<data name="Optional" xml:space="preserve"><value>optional</value></data>
<data name="Pending" xml:space="preserve"><value>Ausstehend</value></data>
<data name="Preview" xml:space="preserve"><value>Vorschau</value></data>
<data name="PrivacyPolicy" xml:space="preserve"><value>Datenschutz</value></data>
<data name="Receiver" xml:space="preserve"><value>Empfänger</value></data>
<data name="ReceiverLogin_AccessCode_Placeholder" xml:space="preserve"><value>Zugangscode eingeben</value></data>
<data name="ReceiverLogin_Description" xml:space="preserve"><value>Bitte geben Sie den Zugangscode ein, den Sie per E-Mail erhalten haben, um das Dokument sicher zu öffnen.</value></data>
<data name="ReceiverLogin_FooterHelp" xml:space="preserve"><value>Bei Problemen wenden Sie sich bitte an den Absender des Dokuments.</value></data>
<data name="ReceiverLogin_InvalidCode_Body" xml:space="preserve"><value>Der eingegebene Code ist falsch. Bitte versuchen Sie es erneut.</value></data>
<data name="ReceiverLogin_InvalidCode_Title" xml:space="preserve"><value>Ungültiger Zugangscode.</value></data>
<data name="ReceiverLogin_NotFound_Body" xml:space="preserve"><value>Der angegebene Zugangscode konnte keinem Dokument zugeordnet werden. Bitte prüfen Sie den Link in Ihrer E-Mail.</value></data>
<data name="ReceiverLogin_NotFound_Title" xml:space="preserve"><value>Dokument nicht gefunden.</value></data>
<data name="ReceiverLogin_Subtitle" xml:space="preserve"><value>Sicherer Zugang mit Zugangscode</value></data>
<data name="ReceiverSigned_ConfirmBody" xml:space="preserve"><value>Diese Aktion kann nicht rückgängig gemacht werden. Mit der Bestätigung erklären Sie, das oben angezeigte Dokument elektronisch unterzeichnet zu haben. Das unterzeichnete Dokument wird anschließend an alle beteiligten Parteien übermittelt.</value></data>
<data name="ReceiverSigned_ConfirmQuestion" xml:space="preserve"><value>Möchten Sie das Dokument verbindlich unterschreiben?</value></data>
<data name="ReceiverSigned_EnvelopeKeyMissing" xml:space="preserve"><value>Envelope-Schlüssel fehlt.</value></data>
<data name="Refresh" xml:space="preserve"><value>Aktualisieren</value></data>
<data name="RequiredField" xml:space="preserve"><value>Pflichtfeld</value></data>
<data name="Selected" xml:space="preserve"><value>ausgewählte</value></data>
<data name="Send" xml:space="preserve"><value>Senden</value></data>
<data name="Sender_ActiveEnvelopes" xml:space="preserve"><value>Aktive Umschläge</value></data>
<data name="Sender_CompletedEnvelopes" xml:space="preserve"><value>Abgeschlossene Umschläge</value></data>
<data name="Sender_CreateEnvelope" xml:space="preserve"><value>Neuer Umschlag</value></data>
<data name="Sender_CreateEnvelope_Title" xml:space="preserve"><value>Neuen Umschlag erstellen</value></data>
<data name="Sender_DeleteEnvelope_Header" xml:space="preserve"><value>Umschlag löschen</value></data>
<data name="Sender_DeleteEnvelope_ReasonPlaceholder" xml:space="preserve"><value>Begründung eingeben...</value></data>
<data name="Sender_DeleteEnvelope_ReasonPrompt" xml:space="preserve"><value>Bitte geben Sie einen Grund an, warum der Umschlag gelöscht/zurückgezogen werden soll.</value></data>
<data name="Sender_DeleteEnvelope_ReasonRequired" xml:space="preserve"><value>Bitte geben Sie einen Löschgrund an.</value></data>
<data name="Sender_DeleteEnvelope_Title" xml:space="preserve"><value>Ausgewählten Umschlag löschen</value></data>
<data name="Sender_Edit" xml:space="preserve"><value>Bearbeiten</value></data>
<data name="Sender_EditEnvelope_Title" xml:space="preserve"><value>Ausgewählten Umschlag bearbeiten</value></data>
<data name="Sender_ExportCurrentView_Title" xml:space="preserve"><value>Aktuelle Ansicht exportieren</value></data>
<data name="Sender_ExportFormat_Aria" xml:space="preserve"><value>Exportformat auswählen</value></data>
<data name="Sender_ExportView_Title" xml:space="preserve"><value>Export der aktuellen Tabellenansicht</value></data>
<data name="Sender_LoadError_Title" xml:space="preserve"><value>Fehler beim Laden der Umschläge</value></data>
<data name="Sender_LoadingEnvelopes" xml:space="preserve"><value>Umschläge werden geladen...</value></data>
<data name="Sender_NoHistory" xml:space="preserve"><value>Keine Historie vorhanden</value></data>
<data name="Sender_NoReceivers" xml:space="preserve"><value>Keine Empfänger</value></data>
<data name="Sender_Preview_Title" xml:space="preserve"><value>Ausgewählten Umschlag als PDF anzeigen</value></data>
<data name="Sender_PreviewDocumentNotFound" xml:space="preserve"><value>Das Dokument für Umschlag #{0} wurde nicht gefunden oder Sie haben keinen Zugriff darauf.</value></data>
<data name="SenderEditor_AddSignature" xml:space="preserve"><value>Signatur hinzufügen</value></data>
<data name="SenderEditor_ClickDocumentFor" xml:space="preserve"><value>Klicken Sie auf die Stelle im Dokument für</value></data>
<data name="SenderEditor_EmailSuggestionsHint" xml:space="preserve"><value>Bereits verwendete E-Mail-Adressen werden bei der Eingabe vorgeschlagen.</value></data>
<data name="SenderEditor_EmailSuggestionsLoading" xml:space="preserve"><value>E-Mail-Vorschläge werden geladen...</value></data>
<data name="SenderEditor_ErrorCreatingEnvelope" xml:space="preserve"><value>Fehler beim Erstellen des Umschlags</value></data>
<data name="SenderEditor_LoadEnvelopeError" xml:space="preserve"><value>Fehler beim Laden des Umschlags #{0}: {1}</value></data>
<data name="SenderEditor_LoadFileError" xml:space="preserve"><value>Fehler beim Laden der Datei: {0}</value></data>
<data name="SenderEditor_Message_Placeholder" xml:space="preserve"><value>Bitte unterzeichnen Sie das beigefügte Dokument.</value></data>
<data name="SenderEditor_NoDocumentLoaded" xml:space="preserve"><value>Kein Dokument geladen</value></data>
<data name="SenderEditor_NoPagesToDisplay" xml:space="preserve"><value>Keine Seiten zur Anzeige verfügbar.</value></data>
<data name="SenderEditor_NoReceiversAdded" xml:space="preserve"><value>Noch keine Empfänger hinzugefügt.</value></data>
<data name="SenderEditor_PageLabel" xml:space="preserve"><value>Seite</value></data>
<data name="SenderEditor_PlaceSignatureField" xml:space="preserve"><value>Signaturfeld platzieren</value></data>
<data name="SenderEditor_RemoveAllSignatureFields" xml:space="preserve"><value>Alle Signaturfelder entfernen</value></data>
<data name="SenderEditor_SelectPdf" xml:space="preserve"><value>Bitte wählen Sie eine PDF-Datei aus.</value></data>
<data name="SenderEditor_Sent_Body" xml:space="preserve"><value>Der bestehende Entwurf wurde versendet.</value></data>
<data name="SenderEditor_Sent_Header" xml:space="preserve"><value>Umschlag gesendet</value></data>
<data name="SenderEditor_Sent_Title" xml:space="preserve"><value>Umschlag wurde erfolgreich gesendet.</value></data>
<data name="SenderEditor_SignatureField" xml:space="preserve"><value>Signaturfeld</value></data>
<data name="SenderEditor_Title_Placeholder" xml:space="preserve"><value>z.B. Arbeitsvertrag 2025</value></data>
<data name="SenderEditor_ToOverview" xml:space="preserve"><value>Zur Übersicht</value></data>
<data name="SenderEditor_UploadPdfToPlaceFields" xml:space="preserve"><value>Laden Sie eine PDF-Datei hoch, um Signaturfelder zu platzieren.</value></data>
<data name="SenderEditor_Validation_EmailDuplicate" xml:space="preserve"><value>Diese E-Mail-Adresse wurde bereits hinzugefügt.</value></data>
<data name="SenderEditor_Validation_EmailInvalid" xml:space="preserve"><value>Bitte geben Sie eine gültige E-Mail-Adresse ein.</value></data>
<data name="SenderEditor_Validation_EmailRequired" xml:space="preserve"><value>Bitte geben Sie eine E-Mail-Adresse ein.</value></data>
<data name="SenderEditor_Validation_FullNameRequired" xml:space="preserve"><value>Bitte geben Sie einen Vor- und Nachnamen ein.</value></data>
<data name="SenderEditor_Validation_ReceiverRequired" xml:space="preserve"><value>Bitte fügen Sie mindestens einen Empfänger hinzu.</value></data>
<data name="SenderEditor_Validation_SignatureFieldRequired" xml:space="preserve"><value>Bitte platzieren Sie mindestens ein Signaturfeld.</value></data>
<data name="SenderEditor_Validation_TitleRequired" xml:space="preserve"><value>Bitte geben Sie einen Titel ein.</value></data>
<data name="SenderEditor_Validation_UploadPdf" xml:space="preserve"><value>Bitte laden Sie zuerst ein PDF-Dokument hoch.</value></data>
<data name="SenderLogin_Description" xml:space="preserve"><value>Bitte melden Sie sich mit Ihren Zugangsdaten an, um auf das Sender-Dashboard zuzugreifen.</value></data>
<data name="SenderLogin_FooterHelp" xml:space="preserve"><value>Bei Problemen wenden Sie sich bitte an den Administrator.</value></data>
<data name="SenderLogin_InvalidCredentials_Body" xml:space="preserve"><value>Benutzername oder Passwort ist falsch. Bitte versuchen Sie es erneut.</value></data>
<data name="SenderLogin_InvalidCredentials_Title" xml:space="preserve"><value>Ungültige Anmeldedaten.</value></data>
<data name="SenderLogin_Password_Placeholder" xml:space="preserve"><value>Passwort eingeben</value></data>
<data name="SenderLogin_Subtitle" xml:space="preserve"><value>Sicherer Zugang zum Sender-Dashboard</value></data>
<data name="SenderLogin_Title" xml:space="preserve"><value>Sender Anmeldung</value></data>
<data name="SenderLogin_Username_Placeholder" xml:space="preserve"><value>Benutzername eingeben</value></data>
<data name="ServerError" xml:space="preserve"><value>Serverfehler.</value></data>
<data name="Signed" xml:space="preserve"><value>Unterschrieben</value></data>
<data name="SignedDocument" xml:space="preserve"><value>Signiertes Dokument</value></data>
<data name="System" xml:space="preserve"><value>System</value></data>
<data name="UnexpectedErrorTryAgainLater" xml:space="preserve"><value>Ein unerwarteter Fehler ist aufgetreten. Bitte versuchen Sie es später erneut.</value></data>
<data name="UploadPdf" xml:space="preserve"><value>PDF hochladen</value></data>
<data name="UserName" xml:space="preserve"><value>Benutzername</value></data>
<data name="VerifyInProgress" xml:space="preserve"><value>Überprüfen ...</value></data>
</root>

View File

@@ -477,4 +477,299 @@
<data name="Confirmations" xml:space="preserve"> <data name="Confirmations" xml:space="preserve">
<value>Confirmations</value> <value>Confirmations</value>
</data> </data>
</root> <data name="NewEnvelope" xml:space="preserve">
<value>New Envelope</value>
</data>
<data name="LoadEnvelope" xml:space="preserve">
<value>Load Envelope</value>
</data>
<data name="DeleteEnvelope" xml:space="preserve">
<value>Delete Envelope</value>
</data>
<data name="RefreshData" xml:space="preserve">
<value>Reload Data</value>
</data>
<data name="RefreshedAt" xml:space="preserve">
<value>Refreshed: {0}</value>
</data>
<data name="ShowDocument" xml:space="preserve">
<value>Show Document</value>
</data>
<data name="ContactReceiver" xml:space="preserve">
<value>Contact Receiver</value>
</data>
<data name="EnvelopeId" xml:space="preserve">
<value>Envelope-ID: {0}</value>
</data>
<data name="OpenLogDirectory" xml:space="preserve">
<value>Open Log Directory</value>
</data>
<data name="ShowResultsReport" xml:space="preserve">
<value>Show Results Report</value>
</data>
<data name="SupportMail" xml:space="preserve">
<value>Support Mail</value>
</data>
<data name="ResendInvitation" xml:space="preserve">
<value>Send Invitation Again</value>
</data>
<data name="Export" xml:space="preserve">
<value>Export</value>
</data>
<data name="Receivers" xml:space="preserve">
<value>Receivers</value>
</data>
<data name="EmailSalutation" xml:space="preserve">
<value>Email Salutation</value>
</data>
<data name="SignedWhen" xml:space="preserve">
<value>Signed When</value>
</data>
<data name="AccessCode" xml:space="preserve">
<value>Access Code</value>
</data>
<data name="User" xml:space="preserve">
<value>User</value>
</data>
<data name="Type" xml:space="preserve">
<value>Type</value>
</data>
<data name="Title" xml:space="preserve">
<value>Title</value>
</data>
<data name="CreatedOn" xml:space="preserve">
<value>Created On</value>
</data>
<data name="LastModified" xml:space="preserve">
<value>Last Modified</value>
</data>
<data name="OpenEnvelopes" xml:space="preserve">
<value>Open Envelopes</value>
</data>
<data name="CompletedEnvelopes" xml:space="preserve">
<value>Completed Envelopes</value>
</data>
<data name="SendAccessCode" xml:space="preserve">
<value>Send Access Code</value>
</data>
<data name="TwoFactorProperties" xml:space="preserve">
<value>2-Factor Properties</value>
</data>
<data name="Name" xml:space="preserve">
<value>Name</value>
</data>
<data name="PhoneNumber" xml:space="preserve">
<value>Phone Number</value>
</data>
<data name="AddReceiver" xml:space="preserve">
<value>Add Receiver</value>
</data>
<data name="DeleteReceiver" xml:space="preserve">
<value>Delete Receiver</value>
</data>
<data name="AddFile" xml:space="preserve">
<value>Add File</value>
</data>
<data name="MergeFiles" xml:space="preserve">
<value>Merge Files</value>
</data>
<data name="DeleteFile" xml:space="preserve">
<value>Delete File</value>
</data>
<data name="ShowFile" xml:space="preserve">
<value>Show File</value>
</data>
<data name="EditFields" xml:space="preserve">
<value>Edit Fields</value>
</data>
<data name="EditData" xml:space="preserve">
<value>Edit Data</value>
</data>
<data name="Save" xml:space="preserve">
<value>Save</value>
</data>
<data name="SendEnvelope" xml:space="preserve">
<value>Send Envelope</value>
</data>
<data name="Cancel" xml:space="preserve">
<value>Cancel</value>
</data>
<data name="AddSignature" xml:space="preserve">
<value>Add Signature</value>
</data>
<data name="DeleteSignature" xml:space="preserve">
<value>Delete Signature</value>
</data>
<data name="Language" xml:space="preserve">
<value>Language</value>
</data>
<data name="UseAccessCode" xml:space="preserve">
<value>Use Access Code</value>
</data>
<data name="TwoFactorEnabled" xml:space="preserve">
<value>2-Factor Authentication Enabled</value>
</data>
<data name="CertificationType" xml:space="preserve">
<value>Certification Type</value>
</data>
<data name="FinalEmailToCreator" xml:space="preserve">
<value>Final Email to Creator</value>
</data>
<data name="FinalEmailToReceivers" xml:space="preserve">
<value>Final Email to Receivers</value>
</data>
<data name="SendReminderEmails" xml:space="preserve">
<value>Send Reminder Emails</value>
</data>
<data name="FirstReminderDays" xml:space="preserve">
<value>First Reminder (Days)</value>
</data>
<data name="ReminderIntervalDays" xml:space="preserve">
<value>Reminder Interval (Days)</value>
</data>
<data name="ExpiresWhenDays" xml:space="preserve">
<value>Expires After (Days)</value>
</data>
<data name="ExpiresWarningDays" xml:space="preserve">
<value>Expiry Warning (Days)</value>
</data>
<data name="Message" xml:space="preserve">
<value>Message</value>
</data>
<data name="EnvelopeType" xml:space="preserve">
<value>Envelope Type</value>
</data>
<data name="AllOptions" xml:space="preserve">
<value>All Options</value>
</data>
<data name="DeleteReason" xml:space="preserve">
<value>Deletion Reason</value>
</data>
<data name="PleaseProvideReason" xml:space="preserve">
<value>Please provide a reason</value>
</data>
<data name="Status" xml:space="preserve">
<value>Status</value>
</data>
<data name="ChangedAt" xml:space="preserve"><value>Changed at</value></data>
<data name="Close" xml:space="preserve"><value>Close</value></data>
<data name="CloseLanguageMenu" xml:space="preserve"><value>Close language menu</value></data>
<data name="ConfirmSignature" xml:space="preserve"><value>Confirm signature</value></data>
<data name="Created" xml:space="preserve"><value>Created</value></data>
<data name="Delete" xml:space="preserve"><value>Delete</value></data>
<data name="Document" xml:space="preserve"><value>Document</value></data>
<data name="DocumentCouldNotBeLoaded" xml:space="preserve"><value>Document could not be loaded</value></data>
<data name="DocumentLoading" xml:space="preserve"><value>Document is loading...</value></data>
<data name="EmailAddress" xml:space="preserve"><value>Email address</value></data>
<data name="Envelope" xml:space="preserve"><value>Envelope</value></data>
<data name="Error" xml:space="preserve"><value>Error</value></data>
<data name="ErrorLoadingDocument_WithReason" xml:space="preserve"><value>Error loading document: {0}</value></data>
<data name="ErrorPage_DevelopmentMode" xml:space="preserve"><value>Development Mode</value></data>
<data name="ErrorPage_DevelopmentMode_Detail" xml:space="preserve"><value>This can result in sensitive exception information being shown to end users. For local debugging, set ASPNETCORE_ENVIRONMENT to Development and restart the app.</value></data>
<data name="ErrorPage_DevelopmentMode_Info" xml:space="preserve"><value>Switching to Development will display more detailed error information.</value></data>
<data name="ErrorPage_DevelopmentMode_Warning" xml:space="preserve"><value>The Development environment should not be enabled in deployed applications.</value></data>
<data name="ErrorPage_ProcessingRequest" xml:space="preserve"><value>An error occurred while processing your request.</value></data>
<data name="ErrorPage_RequestId" xml:space="preserve"><value>Request ID</value></data>
<data name="ErrorWithReason" xml:space="preserve"><value>Error: {0}</value></data>
<data name="From" xml:space="preserve"><value>From</value></data>
<data name="FullName" xml:space="preserve"><value>First and last name</value></data>
<data name="History" xml:space="preserve"><value>History</value></data>
<data name="Index_DigitalSignature" xml:space="preserve"><value>Digital signature</value></data>
<data name="Index_PdfExport" xml:space="preserve"><value>PDF export</value></data>
<data name="Index_SecureAccess" xml:space="preserve"><value>Secure access</value></data>
<data name="Index_Subtitle" xml:space="preserve"><value>Welcome to the eSign portal</value></data>
<data name="Index_ToEnvelopeOverview" xml:space="preserve"><value>Go to envelope overview</value></data>
<data name="Languages" xml:space="preserve"><value>Languages</value></data>
<data name="Loading" xml:space="preserve"><value>Loading...</value></data>
<data name="Login" xml:space="preserve"><value>Sign in</value></data>
<data name="LoginInProgress" xml:space="preserve"><value>Signing in ...</value></data>
<data name="NotAuthenticatedPleaseLoginAgain" xml:space="preserve"><value>Not authenticated. Please sign in again.</value></data>
<data name="Only" xml:space="preserve"><value>Only</value></data>
<data name="OpenDocument" xml:space="preserve"><value>Open document</value></data>
<data name="Optional" xml:space="preserve"><value>optional</value></data>
<data name="Pending" xml:space="preserve"><value>Pending</value></data>
<data name="Preview" xml:space="preserve"><value>Preview</value></data>
<data name="PrivacyPolicy" xml:space="preserve"><value>Privacy policy</value></data>
<data name="Receiver" xml:space="preserve"><value>Receiver</value></data>
<data name="ReceiverLogin_AccessCode_Placeholder" xml:space="preserve"><value>Enter access code</value></data>
<data name="ReceiverLogin_Description" xml:space="preserve"><value>Please enter the access code you received by email to securely open the document.</value></data>
<data name="ReceiverLogin_FooterHelp" xml:space="preserve"><value>If you have issues, please contact the document sender.</value></data>
<data name="ReceiverLogin_InvalidCode_Body" xml:space="preserve"><value>The entered code is incorrect. Please try again.</value></data>
<data name="ReceiverLogin_InvalidCode_Title" xml:space="preserve"><value>Invalid access code.</value></data>
<data name="ReceiverLogin_NotFound_Body" xml:space="preserve"><value>The provided access code could not be matched to a document. Please verify the link in your email.</value></data>
<data name="ReceiverLogin_NotFound_Title" xml:space="preserve"><value>Document not found.</value></data>
<data name="ReceiverLogin_Subtitle" xml:space="preserve"><value>Secure access with access code</value></data>
<data name="ReceiverSigned_ConfirmBody" xml:space="preserve"><value>This action cannot be undone. By confirming, you declare that you have electronically signed the document shown above. The signed document will then be delivered to all involved parties.</value></data>
<data name="ReceiverSigned_ConfirmQuestion" xml:space="preserve"><value>Do you want to sign this document as binding?</value></data>
<data name="ReceiverSigned_EnvelopeKeyMissing" xml:space="preserve"><value>Envelope key is missing.</value></data>
<data name="Refresh" xml:space="preserve"><value>Refresh</value></data>
<data name="RequiredField" xml:space="preserve"><value>Required field</value></data>
<data name="Selected" xml:space="preserve"><value>selected</value></data>
<data name="Send" xml:space="preserve"><value>Send</value></data>
<data name="Sender_ActiveEnvelopes" xml:space="preserve"><value>Active envelopes</value></data>
<data name="Sender_CompletedEnvelopes" xml:space="preserve"><value>Completed envelopes</value></data>
<data name="Sender_CreateEnvelope" xml:space="preserve"><value>New envelope</value></data>
<data name="Sender_CreateEnvelope_Title" xml:space="preserve"><value>Create new envelope</value></data>
<data name="Sender_DeleteEnvelope_Header" xml:space="preserve"><value>Delete envelope</value></data>
<data name="Sender_DeleteEnvelope_ReasonPlaceholder" xml:space="preserve"><value>Enter reason...</value></data>
<data name="Sender_DeleteEnvelope_ReasonPrompt" xml:space="preserve"><value>Please provide a reason why the envelope should be deleted/withdrawn.</value></data>
<data name="Sender_DeleteEnvelope_ReasonRequired" xml:space="preserve"><value>Please provide a deletion reason.</value></data>
<data name="Sender_DeleteEnvelope_Title" xml:space="preserve"><value>Delete selected envelope</value></data>
<data name="Sender_Edit" xml:space="preserve"><value>Edit</value></data>
<data name="Sender_EditEnvelope_Title" xml:space="preserve"><value>Edit selected envelope</value></data>
<data name="Sender_ExportCurrentView_Title" xml:space="preserve"><value>Export current view</value></data>
<data name="Sender_ExportFormat_Aria" xml:space="preserve"><value>Select export format</value></data>
<data name="Sender_ExportView_Title" xml:space="preserve"><value>Export current table view</value></data>
<data name="Sender_LoadError_Title" xml:space="preserve"><value>Error loading envelopes</value></data>
<data name="Sender_LoadingEnvelopes" xml:space="preserve"><value>Loading envelopes...</value></data>
<data name="Sender_NoHistory" xml:space="preserve"><value>No history available</value></data>
<data name="Sender_NoReceivers" xml:space="preserve"><value>No receivers</value></data>
<data name="Sender_Preview_Title" xml:space="preserve"><value>Show selected envelope as PDF</value></data>
<data name="Sender_PreviewDocumentNotFound" xml:space="preserve"><value>The document for envelope #{0} was not found or you do not have access.</value></data>
<data name="SenderEditor_AddSignature" xml:space="preserve"><value>Add signature</value></data>
<data name="SenderEditor_ClickDocumentFor" xml:space="preserve"><value>Click on the location in the document for</value></data>
<data name="SenderEditor_EmailSuggestionsHint" xml:space="preserve"><value>Previously used email addresses are suggested while typing.</value></data>
<data name="SenderEditor_EmailSuggestionsLoading" xml:space="preserve"><value>Loading email suggestions...</value></data>
<data name="SenderEditor_ErrorCreatingEnvelope" xml:space="preserve"><value>Error creating envelope</value></data>
<data name="SenderEditor_LoadEnvelopeError" xml:space="preserve"><value>Error loading envelope #{0}: {1}</value></data>
<data name="SenderEditor_LoadFileError" xml:space="preserve"><value>Error loading file: {0}</value></data>
<data name="SenderEditor_Message_Placeholder" xml:space="preserve"><value>Please sign the attached document.</value></data>
<data name="SenderEditor_NoDocumentLoaded" xml:space="preserve"><value>No document loaded</value></data>
<data name="SenderEditor_NoPagesToDisplay" xml:space="preserve"><value>No pages to display.</value></data>
<data name="SenderEditor_NoReceiversAdded" xml:space="preserve"><value>No receivers added yet.</value></data>
<data name="SenderEditor_PageLabel" xml:space="preserve"><value>Page</value></data>
<data name="SenderEditor_PlaceSignatureField" xml:space="preserve"><value>Place signature field</value></data>
<data name="SenderEditor_RemoveAllSignatureFields" xml:space="preserve"><value>Remove all signature fields</value></data>
<data name="SenderEditor_SelectPdf" xml:space="preserve"><value>Please select a PDF file.</value></data>
<data name="SenderEditor_Sent_Body" xml:space="preserve"><value>The existing draft has been sent.</value></data>
<data name="SenderEditor_Sent_Header" xml:space="preserve"><value>Envelope sent</value></data>
<data name="SenderEditor_Sent_Title" xml:space="preserve"><value>Envelope was sent successfully.</value></data>
<data name="SenderEditor_SignatureField" xml:space="preserve"><value>Signature field</value></data>
<data name="SenderEditor_Title_Placeholder" xml:space="preserve"><value>e.g. Employment contract 2025</value></data>
<data name="SenderEditor_ToOverview" xml:space="preserve"><value>To overview</value></data>
<data name="SenderEditor_UploadPdfToPlaceFields" xml:space="preserve"><value>Upload a PDF file to place signature fields.</value></data>
<data name="SenderEditor_Validation_EmailDuplicate" xml:space="preserve"><value>This email address has already been added.</value></data>
<data name="SenderEditor_Validation_EmailInvalid" xml:space="preserve"><value>Please enter a valid email address.</value></data>
<data name="SenderEditor_Validation_EmailRequired" xml:space="preserve"><value>Please enter an email address.</value></data>
<data name="SenderEditor_Validation_FullNameRequired" xml:space="preserve"><value>Please enter first and last name.</value></data>
<data name="SenderEditor_Validation_ReceiverRequired" xml:space="preserve"><value>Please add at least one receiver.</value></data>
<data name="SenderEditor_Validation_SignatureFieldRequired" xml:space="preserve"><value>Please place at least one signature field.</value></data>
<data name="SenderEditor_Validation_TitleRequired" xml:space="preserve"><value>Please enter a title.</value></data>
<data name="SenderEditor_Validation_UploadPdf" xml:space="preserve"><value>Please upload a PDF document first.</value></data>
<data name="SenderLogin_Description" xml:space="preserve"><value>Please sign in with your credentials to access the sender dashboard.</value></data>
<data name="SenderLogin_FooterHelp" xml:space="preserve"><value>If you have issues, please contact the administrator.</value></data>
<data name="SenderLogin_InvalidCredentials_Body" xml:space="preserve"><value>Username or password is incorrect. Please try again.</value></data>
<data name="SenderLogin_InvalidCredentials_Title" xml:space="preserve"><value>Invalid credentials.</value></data>
<data name="SenderLogin_Password_Placeholder" xml:space="preserve"><value>Enter password</value></data>
<data name="SenderLogin_Subtitle" xml:space="preserve"><value>Secure access to sender dashboard</value></data>
<data name="SenderLogin_Title" xml:space="preserve"><value>Sender login</value></data>
<data name="SenderLogin_Username_Placeholder" xml:space="preserve"><value>Enter username</value></data>
<data name="ServerError" xml:space="preserve"><value>Server error.</value></data>
<data name="Signed" xml:space="preserve"><value>Signed</value></data>
<data name="SignedDocument" xml:space="preserve"><value>Signed document</value></data>
<data name="System" xml:space="preserve"><value>System</value></data>
<data name="UnexpectedErrorTryAgainLater" xml:space="preserve"><value>An unexpected error occurred. Please try again later.</value></data>
<data name="UploadPdf" xml:space="preserve"><value>Upload PDF</value></data>
<data name="UserName" xml:space="preserve"><value>Username</value></data>
<data name="VerifyInProgress" xml:space="preserve"><value>Verifying ...</value></data>
</root>

View File

@@ -477,4 +477,299 @@
<data name="Confirmations" xml:space="preserve"> <data name="Confirmations" xml:space="preserve">
<value>Confirmations</value> <value>Confirmations</value>
</data> </data>
</root> <data name="NewEnvelope" xml:space="preserve">
<value>Nouvelle enveloppe</value>
</data>
<data name="LoadEnvelope" xml:space="preserve">
<value>Charger l'enveloppe</value>
</data>
<data name="DeleteEnvelope" xml:space="preserve">
<value>Supprimer l'enveloppe</value>
</data>
<data name="RefreshData" xml:space="preserve">
<value>Actualiser les données</value>
</data>
<data name="RefreshedAt" xml:space="preserve">
<value>Actualisé : {0}</value>
</data>
<data name="ShowDocument" xml:space="preserve">
<value>Afficher le document</value>
</data>
<data name="ContactReceiver" xml:space="preserve">
<value>Contacter le destinataire</value>
</data>
<data name="EnvelopeId" xml:space="preserve">
<value>ID d'enveloppe : {0}</value>
</data>
<data name="OpenLogDirectory" xml:space="preserve">
<value>Ouvrir le répertoire des logs</value>
</data>
<data name="ShowResultsReport" xml:space="preserve">
<value>Afficher le rapport de résultats</value>
</data>
<data name="SupportMail" xml:space="preserve">
<value>E-mail de support</value>
</data>
<data name="ResendInvitation" xml:space="preserve">
<value>Renvoyer l'invitation</value>
</data>
<data name="Export" xml:space="preserve">
<value>Exporter</value>
</data>
<data name="Receivers" xml:space="preserve">
<value>Destinataires</value>
</data>
<data name="EmailSalutation" xml:space="preserve">
<value>Formule de politesse</value>
</data>
<data name="SignedWhen" xml:space="preserve">
<value>Signé quand</value>
</data>
<data name="AccessCode" xml:space="preserve">
<value>Code d'accès</value>
</data>
<data name="User" xml:space="preserve">
<value>Utilisateur</value>
</data>
<data name="Type" xml:space="preserve">
<value>Type</value>
</data>
<data name="Title" xml:space="preserve">
<value>Titre</value>
</data>
<data name="CreatedOn" xml:space="preserve">
<value>Créé le</value>
</data>
<data name="LastModified" xml:space="preserve">
<value>Dernière modification</value>
</data>
<data name="OpenEnvelopes" xml:space="preserve">
<value>Enveloppes ouvertes</value>
</data>
<data name="CompletedEnvelopes" xml:space="preserve">
<value>Enveloppes terminées</value>
</data>
<data name="SendAccessCode" xml:space="preserve">
<value>Envoyer le code d'accès</value>
</data>
<data name="TwoFactorProperties" xml:space="preserve">
<value>Propriétés 2-facteurs</value>
</data>
<data name="Name" xml:space="preserve">
<value>Nom</value>
</data>
<data name="PhoneNumber" xml:space="preserve">
<value>Numéro de téléphone</value>
</data>
<data name="AddReceiver" xml:space="preserve">
<value>Ajouter un destinataire</value>
</data>
<data name="DeleteReceiver" xml:space="preserve">
<value>Supprimer le destinataire</value>
</data>
<data name="AddFile" xml:space="preserve">
<value>Ajouter un fichier</value>
</data>
<data name="MergeFiles" xml:space="preserve">
<value>Fusionner les fichiers</value>
</data>
<data name="DeleteFile" xml:space="preserve">
<value>Supprimer le fichier</value>
</data>
<data name="ShowFile" xml:space="preserve">
<value>Afficher le fichier</value>
</data>
<data name="EditFields" xml:space="preserve">
<value>Modifier les champs</value>
</data>
<data name="EditData" xml:space="preserve">
<value>Modifier les données</value>
</data>
<data name="Save" xml:space="preserve">
<value>Enregistrer</value>
</data>
<data name="SendEnvelope" xml:space="preserve">
<value>Envoyer l'enveloppe</value>
</data>
<data name="Cancel" xml:space="preserve">
<value>Annuler</value>
</data>
<data name="AddSignature" xml:space="preserve">
<value>Ajouter une signature</value>
</data>
<data name="DeleteSignature" xml:space="preserve">
<value>Supprimer la signature</value>
</data>
<data name="Language" xml:space="preserve">
<value>Langue</value>
</data>
<data name="UseAccessCode" xml:space="preserve">
<value>Utiliser un code d'accès</value>
</data>
<data name="TwoFactorEnabled" xml:space="preserve">
<value>Authentification à 2 facteurs activée</value>
</data>
<data name="CertificationType" xml:space="preserve">
<value>Type de certification</value>
</data>
<data name="FinalEmailToCreator" xml:space="preserve">
<value>E-mail final au créateur</value>
</data>
<data name="FinalEmailToReceivers" xml:space="preserve">
<value>E-mail final aux destinataires</value>
</data>
<data name="SendReminderEmails" xml:space="preserve">
<value>Envoyer des e-mails de rappel</value>
</data>
<data name="FirstReminderDays" xml:space="preserve">
<value>Premier rappel (jours)</value>
</data>
<data name="ReminderIntervalDays" xml:space="preserve">
<value>Intervalle de rappel (jours)</value>
</data>
<data name="ExpiresWhenDays" xml:space="preserve">
<value>Expire après (jours)</value>
</data>
<data name="ExpiresWarningDays" xml:space="preserve">
<value>Avertissement d'expiration (jours)</value>
</data>
<data name="Message" xml:space="preserve">
<value>Message</value>
</data>
<data name="EnvelopeType" xml:space="preserve">
<value>Type d'enveloppe</value>
</data>
<data name="AllOptions" xml:space="preserve">
<value>Toutes les options</value>
</data>
<data name="DeleteReason" xml:space="preserve">
<value>Motif de suppression</value>
</data>
<data name="PleaseProvideReason" xml:space="preserve">
<value>Veuillez indiquer une raison</value>
</data>
<data name="Status" xml:space="preserve">
<value>Statut</value>
</data>
<data name="ChangedAt" xml:space="preserve"><value>Modifié le</value></data>
<data name="Close" xml:space="preserve"><value>Fermer</value></data>
<data name="CloseLanguageMenu" xml:space="preserve"><value>Fermer le menu des langues</value></data>
<data name="ConfirmSignature" xml:space="preserve"><value>Confirmer la signature</value></data>
<data name="Created" xml:space="preserve"><value>Créé</value></data>
<data name="Delete" xml:space="preserve"><value>Supprimer</value></data>
<data name="Document" xml:space="preserve"><value>Document</value></data>
<data name="DocumentCouldNotBeLoaded" xml:space="preserve"><value>Le document n'a pas pu être chargé</value></data>
<data name="DocumentLoading" xml:space="preserve"><value>Chargement du document...</value></data>
<data name="EmailAddress" xml:space="preserve"><value>Adresse e-mail</value></data>
<data name="Envelope" xml:space="preserve"><value>Enveloppe</value></data>
<data name="Error" xml:space="preserve"><value>Erreur</value></data>
<data name="ErrorLoadingDocument_WithReason" xml:space="preserve"><value>Erreur lors du chargement du document : {0}</value></data>
<data name="ErrorPage_DevelopmentMode" xml:space="preserve"><value>Mode développement</value></data>
<data name="ErrorPage_DevelopmentMode_Detail" xml:space="preserve"><value>Cela peut afficher des informations sensibles d'exception aux utilisateurs finaux. Pour le débogage local, définissez ASPNETCORE_ENVIRONMENT sur Development et redémarrez l'application.</value></data>
<data name="ErrorPage_DevelopmentMode_Info" xml:space="preserve"><value>Le mode développement affiche des informations d'erreur plus détaillées.</value></data>
<data name="ErrorPage_DevelopmentMode_Warning" xml:space="preserve"><value>Le mode développement ne doit pas être activé dans les applications déployées.</value></data>
<data name="ErrorPage_ProcessingRequest" xml:space="preserve"><value>Une erreur s'est produite lors du traitement de votre demande.</value></data>
<data name="ErrorPage_RequestId" xml:space="preserve"><value>ID de requête</value></data>
<data name="ErrorWithReason" xml:space="preserve"><value>Erreur : {0}</value></data>
<data name="From" xml:space="preserve"><value>De</value></data>
<data name="FullName" xml:space="preserve"><value>Nom et prénom</value></data>
<data name="History" xml:space="preserve"><value>Historique</value></data>
<data name="Index_DigitalSignature" xml:space="preserve"><value>Signature numérique</value></data>
<data name="Index_PdfExport" xml:space="preserve"><value>Export PDF</value></data>
<data name="Index_SecureAccess" xml:space="preserve"><value>Accès sécurisé</value></data>
<data name="Index_Subtitle" xml:space="preserve"><value>Bienvenue sur le portail eSign</value></data>
<data name="Index_ToEnvelopeOverview" xml:space="preserve"><value>Aller à l'aperçu des enveloppes</value></data>
<data name="Languages" xml:space="preserve"><value>Langues</value></data>
<data name="Loading" xml:space="preserve"><value>Chargement...</value></data>
<data name="Login" xml:space="preserve"><value>Connexion</value></data>
<data name="LoginInProgress" xml:space="preserve"><value>Connexion ...</value></data>
<data name="NotAuthenticatedPleaseLoginAgain" xml:space="preserve"><value>Non authentifié. Veuillez vous reconnecter.</value></data>
<data name="Only" xml:space="preserve"><value>Uniquement</value></data>
<data name="OpenDocument" xml:space="preserve"><value>Ouvrir le document</value></data>
<data name="Optional" xml:space="preserve"><value>optionnel</value></data>
<data name="Pending" xml:space="preserve"><value>En attente</value></data>
<data name="Preview" xml:space="preserve"><value>Aperçu</value></data>
<data name="PrivacyPolicy" xml:space="preserve"><value>Politique de confidentialité</value></data>
<data name="Receiver" xml:space="preserve"><value>Destinataire</value></data>
<data name="ReceiverLogin_AccessCode_Placeholder" xml:space="preserve"><value>Saisir le code d'accès</value></data>
<data name="ReceiverLogin_Description" xml:space="preserve"><value>Veuillez saisir le code d'accès reçu par e-mail pour ouvrir le document en toute sécurité.</value></data>
<data name="ReceiverLogin_FooterHelp" xml:space="preserve"><value>En cas de problème, veuillez contacter l'expéditeur du document.</value></data>
<data name="ReceiverLogin_InvalidCode_Body" xml:space="preserve"><value>Le code saisi est incorrect. Veuillez réessayer.</value></data>
<data name="ReceiverLogin_InvalidCode_Title" xml:space="preserve"><value>Code d'accès invalide.</value></data>
<data name="ReceiverLogin_NotFound_Body" xml:space="preserve"><value>Le code d'accès indiqué ne correspond à aucun document. Veuillez vérifier le lien dans votre e-mail.</value></data>
<data name="ReceiverLogin_NotFound_Title" xml:space="preserve"><value>Document introuvable.</value></data>
<data name="ReceiverLogin_Subtitle" xml:space="preserve"><value>Accès sécurisé avec code d'accès</value></data>
<data name="ReceiverSigned_ConfirmBody" xml:space="preserve"><value>Cette action est irréversible. En confirmant, vous déclarez avoir signé électroniquement le document affiché ci-dessus. Le document signé sera ensuite transmis à toutes les parties concernées.</value></data>
<data name="ReceiverSigned_ConfirmQuestion" xml:space="preserve"><value>Voulez-vous signer ce document de manière contraignante ?</value></data>
<data name="ReceiverSigned_EnvelopeKeyMissing" xml:space="preserve"><value>La clé d'enveloppe est manquante.</value></data>
<data name="Refresh" xml:space="preserve"><value>Actualiser</value></data>
<data name="RequiredField" xml:space="preserve"><value>Champ obligatoire</value></data>
<data name="Selected" xml:space="preserve"><value>sélectionnées</value></data>
<data name="Send" xml:space="preserve"><value>Envoyer</value></data>
<data name="Sender_ActiveEnvelopes" xml:space="preserve"><value>Enveloppes actives</value></data>
<data name="Sender_CompletedEnvelopes" xml:space="preserve"><value>Enveloppes terminées</value></data>
<data name="Sender_CreateEnvelope" xml:space="preserve"><value>Nouvelle enveloppe</value></data>
<data name="Sender_CreateEnvelope_Title" xml:space="preserve"><value>Créer une nouvelle enveloppe</value></data>
<data name="Sender_DeleteEnvelope_Header" xml:space="preserve"><value>Supprimer l'enveloppe</value></data>
<data name="Sender_DeleteEnvelope_ReasonPlaceholder" xml:space="preserve"><value>Saisir la raison...</value></data>
<data name="Sender_DeleteEnvelope_ReasonPrompt" xml:space="preserve"><value>Veuillez indiquer la raison pour laquelle l'enveloppe doit être supprimée/retirée.</value></data>
<data name="Sender_DeleteEnvelope_ReasonRequired" xml:space="preserve"><value>Veuillez indiquer un motif de suppression.</value></data>
<data name="Sender_DeleteEnvelope_Title" xml:space="preserve"><value>Supprimer l'enveloppe sélectionnée</value></data>
<data name="Sender_Edit" xml:space="preserve"><value>Modifier</value></data>
<data name="Sender_EditEnvelope_Title" xml:space="preserve"><value>Modifier l'enveloppe sélectionnée</value></data>
<data name="Sender_ExportCurrentView_Title" xml:space="preserve"><value>Exporter la vue actuelle</value></data>
<data name="Sender_ExportFormat_Aria" xml:space="preserve"><value>Sélectionner le format d'export</value></data>
<data name="Sender_ExportView_Title" xml:space="preserve"><value>Exporter la vue actuelle du tableau</value></data>
<data name="Sender_LoadError_Title" xml:space="preserve"><value>Erreur lors du chargement des enveloppes</value></data>
<data name="Sender_LoadingEnvelopes" xml:space="preserve"><value>Chargement des enveloppes...</value></data>
<data name="Sender_NoHistory" xml:space="preserve"><value>Aucun historique disponible</value></data>
<data name="Sender_NoReceivers" xml:space="preserve"><value>Aucun destinataire</value></data>
<data name="Sender_Preview_Title" xml:space="preserve"><value>Afficher l'enveloppe sélectionnée en PDF</value></data>
<data name="Sender_PreviewDocumentNotFound" xml:space="preserve"><value>Le document de l'enveloppe #{0} est introuvable ou vous n'avez pas l'autorisation d'y accéder.</value></data>
<data name="SenderEditor_AddSignature" xml:space="preserve"><value>Ajouter une signature</value></data>
<data name="SenderEditor_ClickDocumentFor" xml:space="preserve"><value>Cliquez sur l'emplacement dans le document pour</value></data>
<data name="SenderEditor_EmailSuggestionsHint" xml:space="preserve"><value>Les adresses e-mail déjà utilisées sont proposées pendant la saisie.</value></data>
<data name="SenderEditor_EmailSuggestionsLoading" xml:space="preserve"><value>Chargement des suggestions d'e-mail...</value></data>
<data name="SenderEditor_ErrorCreatingEnvelope" xml:space="preserve"><value>Erreur lors de la création de l'enveloppe</value></data>
<data name="SenderEditor_LoadEnvelopeError" xml:space="preserve"><value>Erreur lors du chargement de l'enveloppe #{0} : {1}</value></data>
<data name="SenderEditor_LoadFileError" xml:space="preserve"><value>Erreur lors du chargement du fichier : {0}</value></data>
<data name="SenderEditor_Message_Placeholder" xml:space="preserve"><value>Veuillez signer le document ci-joint.</value></data>
<data name="SenderEditor_NoDocumentLoaded" xml:space="preserve"><value>Aucun document chargé</value></data>
<data name="SenderEditor_NoPagesToDisplay" xml:space="preserve"><value>Aucune page à afficher.</value></data>
<data name="SenderEditor_NoReceiversAdded" xml:space="preserve"><value>Aucun destinataire ajouté pour le moment.</value></data>
<data name="SenderEditor_PageLabel" xml:space="preserve"><value>Page</value></data>
<data name="SenderEditor_PlaceSignatureField" xml:space="preserve"><value>Placer un champ de signature</value></data>
<data name="SenderEditor_RemoveAllSignatureFields" xml:space="preserve"><value>Supprimer tous les champs de signature</value></data>
<data name="SenderEditor_SelectPdf" xml:space="preserve"><value>Veuillez sélectionner un fichier PDF.</value></data>
<data name="SenderEditor_Sent_Body" xml:space="preserve"><value>Le brouillon existant a été envoyé.</value></data>
<data name="SenderEditor_Sent_Header" xml:space="preserve"><value>Enveloppe envoyée</value></data>
<data name="SenderEditor_Sent_Title" xml:space="preserve"><value>L'enveloppe a été envoyée avec succès.</value></data>
<data name="SenderEditor_SignatureField" xml:space="preserve"><value>Champ de signature</value></data>
<data name="SenderEditor_Title_Placeholder" xml:space="preserve"><value>p. ex. Contrat de travail 2025</value></data>
<data name="SenderEditor_ToOverview" xml:space="preserve"><value>Vers l'aperçu</value></data>
<data name="SenderEditor_UploadPdfToPlaceFields" xml:space="preserve"><value>Téléversez un fichier PDF pour placer des champs de signature.</value></data>
<data name="SenderEditor_Validation_EmailDuplicate" xml:space="preserve"><value>Cette adresse e-mail a déjà été ajoutée.</value></data>
<data name="SenderEditor_Validation_EmailInvalid" xml:space="preserve"><value>Veuillez saisir une adresse e-mail valide.</value></data>
<data name="SenderEditor_Validation_EmailRequired" xml:space="preserve"><value>Veuillez saisir une adresse e-mail.</value></data>
<data name="SenderEditor_Validation_FullNameRequired" xml:space="preserve"><value>Veuillez saisir le nom et le prénom.</value></data>
<data name="SenderEditor_Validation_ReceiverRequired" xml:space="preserve"><value>Veuillez ajouter au moins un destinataire.</value></data>
<data name="SenderEditor_Validation_SignatureFieldRequired" xml:space="preserve"><value>Veuillez placer au moins un champ de signature.</value></data>
<data name="SenderEditor_Validation_TitleRequired" xml:space="preserve"><value>Veuillez saisir un titre.</value></data>
<data name="SenderEditor_Validation_UploadPdf" xml:space="preserve"><value>Veuillez d'abord téléverser un document PDF.</value></data>
<data name="SenderLogin_Description" xml:space="preserve"><value>Veuillez vous connecter avec vos identifiants pour accéder au tableau de bord expéditeur.</value></data>
<data name="SenderLogin_FooterHelp" xml:space="preserve"><value>En cas de problème, veuillez contacter l'administrateur.</value></data>
<data name="SenderLogin_InvalidCredentials_Body" xml:space="preserve"><value>Le nom d'utilisateur ou le mot de passe est incorrect. Veuillez réessayer.</value></data>
<data name="SenderLogin_InvalidCredentials_Title" xml:space="preserve"><value>Identifiants invalides.</value></data>
<data name="SenderLogin_Password_Placeholder" xml:space="preserve"><value>Saisir le mot de passe</value></data>
<data name="SenderLogin_Subtitle" xml:space="preserve"><value>Accès sécurisé au tableau de bord expéditeur</value></data>
<data name="SenderLogin_Title" xml:space="preserve"><value>Connexion expéditeur</value></data>
<data name="SenderLogin_Username_Placeholder" xml:space="preserve"><value>Saisir le nom d'utilisateur</value></data>
<data name="ServerError" xml:space="preserve"><value>Erreur serveur.</value></data>
<data name="Signed" xml:space="preserve"><value>Signé</value></data>
<data name="SignedDocument" xml:space="preserve"><value>Document signé</value></data>
<data name="System" xml:space="preserve"><value>Système</value></data>
<data name="UnexpectedErrorTryAgainLater" xml:space="preserve"><value>Une erreur inattendue s'est produite. Veuillez réessayer plus tard.</value></data>
<data name="UploadPdf" xml:space="preserve"><value>Téléverser un PDF</value></data>
<data name="UserName" xml:space="preserve"><value>Nom d'utilisateur</value></data>
<data name="VerifyInProgress" xml:space="preserve"><value>Vérification ...</value></data>
</root>

View File

@@ -1,6 +1,7 @@
Imports System.IO Imports System.IO
Imports DigitalData.Modules.Database Imports DigitalData.Modules.Database
Imports DigitalData.Modules.Logging Imports DigitalData.Modules.Logging
Imports EnvelopeGenerator.CommonServices
Imports EnvelopeGenerator.CommonServices.Jobs Imports EnvelopeGenerator.CommonServices.Jobs
Imports EnvelopeGenerator.CommonServices.Jobs.FinalizeDocument Imports EnvelopeGenerator.CommonServices.Jobs.FinalizeDocument
Imports GdPicture14 Imports GdPicture14
@@ -145,64 +146,91 @@ Public Class frmFinalizePDF
log.AppendLine($"Document bytes: {oBuffer.Length}") log.AppendLine($"Document bytes: {oBuffer.Length}")
' 3. Check what BurnAnnotsToPDF will do internally ' 3. Check what BurnAnnotsToPDF will do internally
Dim oEnvelopeEntity As Envelope = Nothing
Using scope = Factory.Shared.ScopeFactory.CreateScope() Using scope = Factory.Shared.ScopeFactory.CreateScope()
Dim envRepo = scope.ServiceProvider.Repository(Of Envelope)() Dim envRepo = scope.ServiceProvider.Repository(Of Envelope)()
Dim envelope = envRepo.Where(Function(env) env.Id = envelopeId).FirstOrDefault() oEnvelopeEntity = envRepo.Where(Function(env) env.Id = envelopeId).FirstOrDefault()
If envelope Is Nothing Then If oEnvelopeEntity Is Nothing Then
log.AppendLine("ERROR: Envelope not found in EF Core!") log.AppendLine("ERROR: Envelope not found in EF Core!")
txtResult.Text = log.ToString() txtResult.Text = log.ToString()
Return Return
End If End If
log.AppendLine($"Envelope found: Id={envelope.Id}, EnvelopeTypeId={envelope.EnvelopeTypeId}") log.AppendLine($"Envelope found: Id={oEnvelopeEntity.Id}, EnvelopeTypeId={oEnvelopeEntity.EnvelopeTypeId}")
log.AppendLine($"ReadOnly (IsReadAndConfirm): {envelope.ReadOnly}") log.AppendLine($"ReadOnly (IsReadAndConfirm): {oEnvelopeEntity.ReadOnly}")
If envelope.ReadOnly Then If oEnvelopeEntity.ReadOnly Then
log.AppendLine(">>> EARLY RETURN: ReadOnly=True, original PDF returned without burning") log.AppendLine(">>> ReadOnly=True: original PDF will be used without burning (annotations skipped)")
txtResult.Text = log.ToString()
Return
End If
Dim sigRepo = scope.ServiceProvider.Repository(Of DocReceiverElement)()
Dim elements = sigRepo _
.Where(Function(sig) sig.Document.EnvelopeId = envelopeId) _
.Include(Function(sig) sig.Annotations) _
.ToList()
log.AppendLine($"Elements (Signature) count: {elements.Count}")
If elements.Any() Then
log.AppendLine(">>> PATH: BurnElementAnnotsToPDF (new element-based path)")
For Each elem In elements
Dim annotCount = If(elem.Annotations IsNot Nothing, elem.Annotations.Count(), 0)
log.AppendLine($" Element Id={elem.Id}, Page={elem.Page}, X={elem.X}, Y={elem.Y}, W={elem.Width}, H={elem.Height}, Annotations={annotCount}")
If elem.Annotations IsNot Nothing Then
For Each annot In elem.Annotations
log.AppendLine($" Annot: Name={annot.Name}, Type={annot.Type}, X={annot.X}, Y={annot.Y}, W={annot.Width}, H={annot.Height}")
Next
End If
Next
Else Else
log.AppendLine(">>> PATH: BurnInstantJSONAnnotsToPDF (old JSON-based path)") Dim sigRepo = scope.ServiceProvider.Repository(Of DocReceiverElement)()
Dim elements = sigRepo _
.Where(Function(sig) sig.Document.EnvelopeId = envelopeId) _
.Include(Function(sig) sig.Annotations) _
.ToList()
log.AppendLine($"Elements (Signature) count: {elements.Count}")
If elements.Any() Then
log.AppendLine(">>> PATH: BurnElementAnnotsToPDF (new element-based path)")
For Each elem In elements
Dim annotCount = If(elem.Annotations IsNot Nothing, elem.Annotations.Count(), 0)
log.AppendLine($" Element Id={elem.Id}, Page={elem.Page}, X={elem.X}, Y={elem.Y}, W={elem.Width}, H={elem.Height}, Annotations={annotCount}")
If elem.Annotations IsNot Nothing Then
For Each annot In elem.Annotations
log.AppendLine($" Annot: Name={annot.Name}, Type={annot.Type}, X={annot.X}, Y={annot.Y}, W={annot.Width}, H={annot.Height}")
Next
End If
Next
Else
log.AppendLine(">>> PATH: BurnInstantJSONAnnotsToPDF (old JSON-based path)")
End If
End If End If
End Using End Using
' 4. Actually call BurnAnnotsToPDF (same as Service) ' 4. Burn annotations (ReadOnly envelopes return original PDF unchanged)
log.AppendLine("") log.AppendLine("")
log.AppendLine("Calling BurnAnnotsToPDF...") log.AppendLine("Calling BurnAnnotsToPDF...")
Dim oNewBuffer = PDFBurner.BurnAnnotsToPDF(oBuffer, oJsonList, envelopeId) Dim oBurnedBuffer As Byte() = PDFBurner.BurnAnnotsToPDF(oBuffer, oJsonList, envelopeId)
log.AppendLine($"Result bytes: {oNewBuffer.Length}") log.AppendLine($"Burned bytes: {oBurnedBuffer.Length}")
log.AppendLine($"Same as input: {oBuffer.Length = oNewBuffer.Length AndAlso oBuffer.SequenceEqual(oNewBuffer)}") log.AppendLine($"Unchanged (ReadOnly): {oBuffer.Length = oBurnedBuffer.Length AndAlso oBuffer.SequenceEqual(oBurnedBuffer)}")
' 5. Write output ' 5. Build State and load Envelope domain model for ReportCreator
log.AppendLine("")
log.AppendLine("Loading envelope domain model for report...")
Dim oState As New State() With {
.Database = Database,
.LogConfig = LogConfig
}
Dim oEnvelopeModel As New EnvelopeModel(oState)
Dim oEnvelope = oEnvelopeModel.GetById(envelopeId)
If oEnvelope Is Nothing Then
log.AppendLine("ERROR: Envelope not found via EnvelopeModel!")
txtResult.Text = log.ToString()
Return
End If
log.AppendLine($"Envelope loaded: {oEnvelope.Id}")
' 6. Create signing certificate report
log.AppendLine("Creating signing certificate report...")
Dim oReportCreator As New ReportCreator(LogConfig, oState)
Dim oReportBuffer As Byte() = oReportCreator.CreateReport(oEnvelope)
log.AppendLine($"Report bytes: {oReportBuffer.Length}")
' 7. Merge burned document + report (last page)
log.AppendLine("Merging documents...")
Dim oMerger As New PDFMerger(LogConfig, pGDPictureLicenseKey)
Dim oMergedBuffer As Byte() = oMerger.MergeDocuments(oBurnedBuffer, oReportBuffer)
log.AppendLine($"Merged bytes: {oMergedBuffer.Length}")
' 8. Save and open
Dim desktopPath As String = Environment.GetFolderPath(Environment.SpecialFolder.Desktop) Dim desktopPath As String = Environment.GetFolderPath(Environment.SpecialFolder.Desktop)
Dim oNewPath = Path.Combine(desktopPath, $"E{txtEnvelope.Text}_FullTest.burned.pdf") Dim oMergedPath = Path.Combine(desktopPath, $"E{txtEnvelope.Text}_FullTest.merged.pdf")
File.WriteAllBytes(oNewPath, oNewBuffer) File.WriteAllBytes(oMergedPath, oMergedBuffer)
log.AppendLine($"Output: {oNewPath}") log.AppendLine($"Output: {oMergedPath}")
txtResult.Text = log.ToString() txtResult.Text = log.ToString()
Process.Start(oNewPath) Process.Start(oMergedPath)
Catch ex As Exception Catch ex As Exception
txtResult.Text = $"ERROR: {ex.Message}{vbCrLf}{vbCrLf}{ex.ToString()}" txtResult.Text = $"ERROR: {ex.Message}{vbCrLf}{vbCrLf}{ex.ToString()}"

View File

@@ -119,7 +119,7 @@ Namespace Jobs.FinalizeDocument
Dim y = frameY + yOffsetofFF Dim y = frameY + yOffsetofFF
If annot.Type = AnnotationType.FormField Then If annot.Type = AnnotationType.FormField Then
AddFormFieldValue(annot.X / inchFactor, y, annot.Width / inchFactor, annot.Height / inchFactor, element.Page, annot.Value) AddFormFieldValue(annot.X / inchFactor, y, annot.Width / inchFactor, annot.Height / inchFactor, element.Page, annot.Value, annot.Name)
ElseIf annot.Type = AnnotationType.Image Then ElseIf annot.Type = AnnotationType.Image Then
AddImageAnnotation( AddImageAnnotation(
annot.X / inchFactor, annot.X / inchFactor,
@@ -269,11 +269,14 @@ Namespace Jobs.FinalizeDocument
Next Next
End Sub End Sub
Private Sub AddFormFieldValue(x As Double, y As Double, width As Double, height As Double, page As Integer, value As String) Private Sub AddFormFieldValue(x As Double, y As Double, width As Double, height As Double, page As Integer, value As String, Optional egName As String = "")
Manager.SelectPage(page) Manager.SelectPage(page)
' Truncate if needed
Dim displayValue As String = TruncateText(value, egName)
' Add the text annotation ' Add the text annotation
Dim ant = Manager.AddTextAnnot(x, y, width, height, value) Dim ant = Manager.AddTextAnnot(x, y, width, height, displayValue)
' Set the font properties ' Set the font properties
ant.FontName = _pdfBurnerParams.FontName ant.FontName = _pdfBurnerParams.FontName
@@ -294,8 +297,9 @@ Namespace Jobs.FinalizeDocument
Dim oHeight = oBounds.Item(3) Dim oHeight = oBounds.Item(3)
Manager.SelectPage(pAnnotation.pageIndex + 1) Manager.SelectPage(pAnnotation.pageIndex + 1)
' Add the text annotation ' Add the text annotation (truncate if needed)
Dim ant = Manager.AddTextAnnot(oX, oY, oWidth, oHeight, formFieldValue.value) Dim displayValue As String = TruncateText(formFieldValue.value, pAnnotation.egName)
Dim ant = Manager.AddTextAnnot(oX, oY, oWidth, oHeight, displayValue)
' Set the font properties ' Set the font properties
ant.FontName = _pdfBurnerParams.FontName ant.FontName = _pdfBurnerParams.FontName
@@ -306,6 +310,48 @@ Namespace Jobs.FinalizeDocument
#End Region #End Region
#Region "Helpers" #Region "Helpers"
''' <summary>
''' Truncates a field value so its rendered width fits within the configured max width (inches).
''' Uses Graphics.MeasureString with the configured font for pixel-accurate measurement.
''' Appends TruncationSuffix when the value is cut.
''' </summary>
Private Function TruncateText(value As String, fieldEgName As String) As String
If String.IsNullOrEmpty(value) Then Return value
If _pdfBurnerParams.TextMaxWidths Is Nothing Then Return value
Dim maxWidthInches As Single = 0
If Not _pdfBurnerParams.TextMaxWidths.TryGetValue(fieldEgName, maxWidthInches) Then Return value
If maxWidthInches <= 0 Then Return value
Dim suffix As String = If(_pdfBurnerParams.TruncationSuffix, "")
Using bmp As New Bitmap(1, 1)
Using g As Graphics = Graphics.FromImage(bmp)
Dim font As New Font(_pdfBurnerParams.FontName, _pdfBurnerParams.FontSize, _pdfBurnerParams.FontStyle, GraphicsUnit.Point)
Dim maxWidthPx As Single = maxWidthInches * g.DpiX
' Already fits — no truncation needed
If g.MeasureString(value, font).Width <= maxWidthPx Then Return value
' Binary search for the longest prefix that fits (with suffix)
Dim lo As Integer = 0
Dim hi As Integer = value.Length
While lo < hi
Dim mid As Integer = (lo + hi + 1) \ 2
Dim candidate As String = value.Substring(0, mid) & suffix
If g.MeasureString(candidate, font).Width <= maxWidthPx Then
lo = mid
Else
hi = mid - 1
End If
End While
Return value.Substring(0, lo) & suffix
End Using
End Using
End Function
Private Function ToPointF(pPoints As List(Of Single)) As PointF Private Function ToPointF(pPoints As List(Of Single)) As PointF
Dim oPoints = pPoints.Select(AddressOf ToInches).ToList() Dim oPoints = pPoints.Select(AddressOf ToInches).ToList()
Return New PointF(oPoints.Item(0), oPoints.Item(1)) Return New PointF(oPoints.Item(0), oPoints.Item(1))

View File

@@ -12,5 +12,24 @@ Namespace Jobs.FinalizeDocument
Public Property FontSize As Integer = 8 Public Property FontSize As Integer = 8
Public Property FontStyle As FontStyle = FontStyle.Italic Public Property FontStyle As FontStyle = FontStyle.Italic
''' <summary>
''' The suffix appended to a truncated field value. Defaults to "..".
''' Can be changed to any string (e.g. "…", "...", "").
''' </summary>
Public Property TruncationSuffix As String = ".."
''' <summary>
''' Per-field maximum render width in inches.
''' Text is measured using the configured Font/FontSize via Graphics.MeasureString (pixel-accurate).
''' If the rendered width exceeds the limit, the text is cut and TruncationSuffix is appended.
''' Keys must match egName values used in PDFBurner (e.g. "name", "position", "city", "date").
''' Set a value to 0 to disable truncation for that field.
''' </summary>
Public Property TextMaxWidths As New Dictionary(Of String, Single) From {
{"position", 1.41F},
{"city", 1.41F},
{"date", 2.0F}
}
End Class End Class
End Namespace End Namespace

View File

@@ -1,5 +1,6 @@
Imports EnvelopeGenerator.Domain.Constants Imports EnvelopeGenerator.Domain.Constants
Imports EnvelopeGenerator.Domain.Entities Imports EnvelopeGenerator.Domain.Entities
Imports EnvelopeGenerator.Domain.Interfaces
Public Class ReportItem Public Class ReportItem
@@ -11,8 +12,15 @@ Public Class ReportItem
Public Property ItemStatus As EnvelopeStatus Public Property ItemStatus As EnvelopeStatus
Public ReadOnly Property ItemStatusTranslated As String Public ReadOnly Property ItemStatusTranslated As String
Get Get
Dim oStatus = ItemStatus.ToString() Dim oStatusKey = ItemStatus.ToString()
Return My.Resources.Model.ResourceManager.GetString(oStatus)
If ItemStatus = EnvelopeStatus.DocumentSigned AndAlso
Envelope IsNot Nothing AndAlso
Envelope.IsReadAndConfirm() Then
oStatusKey = "DocumentSignedRaC"
End If
Return My.Resources.Model.ResourceManager.GetString(oStatusKey)
End Get End Get
End Property End Property

View File

@@ -168,6 +168,9 @@
<data name="DocumentSigned" xml:space="preserve"> <data name="DocumentSigned" xml:space="preserve">
<value>Document signed</value> <value>Document signed</value>
</data> </data>
<data name="DocumentSignedRaC" xml:space="preserve">
<value>Read and confirmed</value>
</data>
<data name="Draft" xml:space="preserve"> <data name="Draft" xml:space="preserve">
<value>Draft</value> <value>Draft</value>
</data> </data>

View File

@@ -168,6 +168,9 @@
<data name="DocumentSigned" xml:space="preserve"> <data name="DocumentSigned" xml:space="preserve">
<value>Dokument unterzeichnet</value> <value>Dokument unterzeichnet</value>
</data> </data>
<data name="DocumentSignedRaC" xml:space="preserve">
<value>Gelesen und bestätigt</value>
</data>
<data name="Draft" xml:space="preserve"> <data name="Draft" xml:space="preserve">
<value>Entwurf</value> <value>Entwurf</value>
</data> </data>

View File

@@ -1,8 +1,10 @@
using System.Collections.Generic; using System;
using System.Collections.Generic;
using System.Linq;
namespace EnvelopeGenerator.Domain.Constants namespace EnvelopeGenerator.Domain.Constants
{ {
// http://wiki.dd/xwiki13/bin/view/Anwendungen/Produkt-Handbuch/Sonstiges/SignFlow/Envelope%20Status/ // http://wiki.dd/xwiki_prod/bin/view/Anwendungen/Produkt-Handbuch/Sonstiges/signFLOW/signFLOW%20-%20Enwickler-Handbuch/4.%20Anhang/4.3%20Historie%20und%20Status%20der%20Umschl%C3%A4ge/
public enum EnvelopeStatus public enum EnvelopeStatus
{ {
Invalid = 0, Invalid = 0,
@@ -49,5 +51,28 @@ namespace EnvelopeGenerator.Domain.Constants
EnvelopeStatus.EnvelopeCreated, EnvelopeStatus.EnvelopeCreated,
EnvelopeStatus.DocumentMod_Rotation EnvelopeStatus.DocumentMod_Rotation
}; };
public static readonly List<EnvelopeStatus> Active = Enum.GetValues(typeof(EnvelopeStatus))
.Cast<EnvelopeStatus>()
.Where(status => status.IsActive())
.ToList();
public static readonly List<EnvelopeStatus> Completed = Enum.GetValues(typeof(EnvelopeStatus))
.Cast<EnvelopeStatus>()
.Where(status => status.IsCompleted())
.ToList();
}
public static class EnvelopeStatusExtensions
{
public static bool IsActive(this EnvelopeStatus status)
{
return status >= EnvelopeStatus.EnvelopeCreated && status < EnvelopeStatus.EnvelopePartlySigned;
}
public static bool IsCompleted(this EnvelopeStatus status)
{
return status >= EnvelopeStatus.EnvelopeCompletelySigned && status <= EnvelopeStatus.EnvelopeWithdrawn;
}
} }
} }

View File

@@ -1,5 +1,6 @@
#if NETFRAMEWORK #if NETFRAMEWORK
using System; using System;
using System.Collections.Generic;
#endif #endif
namespace EnvelopeGenerator.Domain.Constants namespace EnvelopeGenerator.Domain.Constants
@@ -15,7 +16,11 @@ namespace EnvelopeGenerator.Domain.Constants
public static class Receiver public static class Receiver
{ {
public const string TFA = "EGReceiverTFA"; public const string TFA = "EGReceiverTFA";
[Obsolete("Use Receiver.Authorized")]
public const string Full = "EGReceiver"; public const string Full = "EGReceiver";
public static readonly IReadOnlyList<string> Authorized = new[] { "receiver", "EGReceiver" };
} }
public const string Sender = "EGSender"; public const string Sender = "EGSender";

View File

@@ -94,7 +94,7 @@ namespace EnvelopeGenerator.Domain.Entities
public string Language { get; set; } public string Language { get; set; }
[Column("SEND_REMINDER_EMAILS")] [Column("SEND_REMINDER_EMAILS")]
public bool SendReminderEmails { get; set; } public bool? SendReminderEmails { get; set; }
[Column("FIRST_REMINDER_DAYS")] [Column("FIRST_REMINDER_DAYS")]
public int? FirstReminderDays { get; set; } public int? FirstReminderDays { get; set; }
@@ -114,7 +114,7 @@ namespace EnvelopeGenerator.Domain.Entities
public int? CertificationType { get; set; } public int? CertificationType { get; set; }
[Column("USE_ACCESS_CODE")] [Column("USE_ACCESS_CODE")]
public bool UseAccessCode { get; set; } public bool? UseAccessCode { get; set; }
[Column("FINAL_EMAIL_TO_CREATOR")] [Column("FINAL_EMAIL_TO_CREATOR")]
public int? FinalEmailToCreator { get; set; } public int? FinalEmailToCreator { get; set; }
@@ -132,7 +132,7 @@ namespace EnvelopeGenerator.Domain.Entities
public User User { get; set; } public User User { get; set; }
[Column("TFA_ENABLED")] [Column("TFA_ENABLED")]
public bool TfaEnabled { get; set; } public bool? TfaEnabled { get; set; }
#if NETFRAMEWORK #if NETFRAMEWORK
= false; = false;
#endif #endif

View File

@@ -22,41 +22,41 @@ namespace EnvelopeGenerator.Domain.Entities
public string Language { get; set; } public string Language { get; set; }
[Column("EXPIRES_DAYS")] [Column("EXPIRES_DAYS")]
public int ExpiresDays { get; set; } public int? ExpiresDays { get; set; }
[Column("CERTIFICATION_TYPE")] [Column("CERTIFICATION_TYPE")]
public Constants.CertificationType CertificationType { get; set; } = Constants.CertificationType.AdvancedElectronicSignature; public Constants.CertificationType? CertificationType { get; set; }
[Column("USE_ACCESS_CODE")] [Column("USE_ACCESS_CODE")]
public bool UseAccessCode { get; set; } public bool? UseAccessCode { get; set; }
[Column("FINAL_EMAIL_TO_CREATOR")] [Column("FINAL_EMAIL_TO_CREATOR")]
public Constants.FinalEmailType FinalEmailToCreator { get; set; } public Constants.FinalEmailType? FinalEmailToCreator { get; set; }
[Column("FINAL_EMAIL_TO_RECEIVERS")] [Column("FINAL_EMAIL_TO_RECEIVERS")]
public Constants.FinalEmailType FinalEmailToReceivers { get; set; } public Constants.FinalEmailType? FinalEmailToReceivers { get; set; }
[Required] [Required]
[Column("ADDED_WHEN", TypeName = "datetime")] [Column("ADDED_WHEN", TypeName = "datetime")]
public DateTime AddedWhen { get; set; } public DateTime AddedWhen { get; set; }
[Column("CHANGED_WHEN", TypeName = "datetime")] [Column("CHANGED_WHEN", TypeName = "datetime")]
public DateTime ChangedWhen { get; set; } public DateTime? ChangedWhen { get; set; }
[Column("EXPIRES_WARNING_DAYS")] [Column("EXPIRES_WARNING_DAYS")]
public int ExpiresWarningDays { get; set; } public int? ExpiresWarningDays { get; set; }
[Column("SEND_REMINDER_EMAILS")] [Column("SEND_REMINDER_EMAILS")]
public bool SendReminderEmails { get; set; } = false; public bool? SendReminderEmails { get; set; }
[Column("FIRST_REMINDER_DAYS")] [Column("FIRST_REMINDER_DAYS")]
public int FirstReminderDays { get; set; } = 0; public int? FirstReminderDays { get; set; }
[Column("REMINDER_INTERVAL_DAYS")] [Column("REMINDER_INTERVAL_DAYS")]
public int ReminderIntervalDays { get; set; } = 0; public int? ReminderIntervalDays { get; set; }
[Column("CONTRACT_TYPE")] [Column("CONTRACT_TYPE")]
public int ContractType { get; set; } public int? ContractType { get; set; }
[NotMapped] [NotMapped]
public bool TFA_Enabled { get; set; } = false; public bool TFA_Enabled { get; set; } = false;
@@ -70,14 +70,14 @@ namespace EnvelopeGenerator.Domain.Entities
[NotMapped] [NotMapped]
public int ExpiresWhenDays public int ExpiresWhenDays
{ {
get => ExpiresDays; get => ExpiresDays ?? 0;
set => ExpiresDays = value; set => ExpiresDays = value;
} }
[NotMapped] [NotMapped]
public int ExpiresWarningWhenDays public int ExpiresWarningWhenDays
{ {
get => ExpiresWarningDays; get => ExpiresWarningDays ?? 0;
set => ExpiresWarningDays = value; set => ExpiresWarningDays = value;
} }
@@ -86,4 +86,4 @@ namespace EnvelopeGenerator.Domain.Entities
return Title; return Title;
} }
} }
} }

View File

@@ -213,6 +213,15 @@ namespace My.Resources {
} }
} }
/// <summary>
/// Looks up a localized string similar to Gelesen und bestätigt.
/// </summary>
public static string DocumentSignedRaC {
get {
return ResourceManager.GetString("DocumentSignedRaC", resourceCulture);
}
}
/// <summary> /// <summary>
/// Looks up a localized string similar to Entwurf. /// Looks up a localized string similar to Entwurf.
/// </summary> /// </summary>

View File

@@ -0,0 +1,7 @@
namespace EnvelopeGenerator.Domain.Resources
{
// Marker type for IStringLocalizer lookup of Resources/Model.resx.
public class Model
{
}
}

View File

@@ -0,0 +1,116 @@
<?xml version="1.0" encoding="utf-8"?>
<root>
<xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
<xsd:import namespace="http://www.w3.org/XML/1998/namespace" />
<xsd:element name="root" msdata:IsDataSet="true">
<xsd:complexType>
<xsd:choice maxOccurs="unbounded">
<xsd:element name="metadata">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" />
</xsd:sequence>
<xsd:attribute name="name" use="required" type="xsd:string" />
<xsd:attribute name="type" type="xsd:string" />
<xsd:attribute name="mimetype" type="xsd:string" />
<xsd:attribute ref="xml:space" />
</xsd:complexType>
</xsd:element>
<xsd:element name="assembly">
<xsd:complexType>
<xsd:attribute name="alias" type="xsd:string" />
<xsd:attribute name="name" type="xsd:string" />
</xsd:complexType>
</xsd:element>
<xsd:element name="data">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
<xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" msdata:Ordinal="1" />
<xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3" />
<xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4" />
<xsd:attribute ref="xml:space" />
</xsd:complexType>
</xsd:element>
<xsd:element name="resheader">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" />
</xsd:complexType>
</xsd:element>
</xsd:choice>
</xsd:complexType>
</xsd:element>
</xsd:schema>
<resheader name="resmimetype">
<value>text/microsoft-resx</value>
</resheader>
<resheader name="version">
<value>2.0</value>
</resheader>
<resheader name="reader">
<value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<resheader name="writer">
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<data name="AccessCodeIncorrect" xml:space="preserve">
<value>Code d'acces incorrect</value>
</data>
<data name="DocumentRejected" xml:space="preserve">
<value>Document rejete</value>
</data>
<data name="DocumentSigned" xml:space="preserve">
<value>Document signe</value>
</data>
<data name="Draft" xml:space="preserve">
<value>Brouillon</value>
</data>
<data name="EnvelopeCompletelySigned" xml:space="preserve">
<value>Entierement signe</value>
</data>
<data name="EnvelopeCreated" xml:space="preserve">
<value>Enveloppe creee</value>
</data>
<data name="EnvelopeDeleted" xml:space="preserve">
<value>Enveloppe supprimee</value>
</data>
<data name="EnvelopePartlySigned" xml:space="preserve">
<value>Partiellement signee</value>
</data>
<data name="EnvelopeQueued" xml:space="preserve">
<value>Enveloppe en file d'attente</value>
</data>
<data name="EnvelopeRejected" xml:space="preserve">
<value>Enveloppe rejetee</value>
</data>
<data name="EnvelopeSaved" xml:space="preserve">
<value>Enregistree</value>
</data>
<data name="EnvelopeSent" xml:space="preserve">
<value>Envoyee</value>
</data>
<data name="EnvelopeWithdrawn" xml:space="preserve">
<value>Enveloppe retiree</value>
</data>
<data name="MessageAccessCodeSent" xml:space="preserve">
<value>Code d'acces envoye</value>
</data>
<data name="MessageCompletionSent" xml:space="preserve">
<value>E-mail de cloture envoye</value>
</data>
<data name="MessageConfirmationSent" xml:space="preserve">
<value>Confirmation finale envoyee</value>
</data>
<data name="MessageDeletionSent" xml:space="preserve">
<value>Notification de suppression envoyee</value>
</data>
<data name="MessageInvitationSent" xml:space="preserve">
<value>Invitation envoyee</value>
</data>
</root>

View File

@@ -168,6 +168,9 @@
<data name="DocumentSigned" xml:space="preserve"> <data name="DocumentSigned" xml:space="preserve">
<value>Dokument unterzeichnet</value> <value>Dokument unterzeichnet</value>
</data> </data>
<data name="DocumentSignedRaC" xml:space="preserve">
<value>Gelesen und bestätigt</value>
</data>
<data name="Draft" xml:space="preserve"> <data name="Draft" xml:space="preserve">
<value>Entwurf</value> <value>Entwurf</value>
</data> </data>

View File

@@ -0,0 +1,18 @@
using EnvelopeGenerator.Application.Common.Interfaces.Doc;
using Microsoft.Extensions.DependencyInjection;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace EnvelopeGenerator.Infrastructure.Doc;
public static class DependencyInjection
{
public static IServiceCollection AddDocInfrastructure(this IServiceCollection services)
{
services.AddScoped<IPdfPageImageRenderer, PdfPageImageRenderer>();
return services;
}
}

View File

@@ -0,0 +1,17 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\EnvelopeGenerator.Application\EnvelopeGenerator.Application.csproj" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="DevExpress.Document.Processor" Version="25.2.3" />
</ItemGroup>
</Project>

View File

@@ -0,0 +1,57 @@
using DevExpress.Drawing;
using DevExpress.Pdf;
using EnvelopeGenerator.Application.Common.Dto;
using EnvelopeGenerator.Application.Common.Interfaces.Doc;
namespace EnvelopeGenerator.Infrastructure.Doc;
public class PdfPageImageRenderer : IPdfPageImageRenderer
{
private const float RenderDpi = 110f;
public Task<IReadOnlyList<PdfPageImageDto>> RenderPagesAsImagesAsync(byte[] pdfBytes, CancellationToken cancellationToken = default)
{
ArgumentNullException.ThrowIfNull(pdfBytes);
if (pdfBytes.Length == 0)
return Task.FromResult<IReadOnlyList<PdfPageImageDto>>(Array.Empty<PdfPageImageDto>());
using var stream = new MemoryStream(pdfBytes, writable: false);
using var processor = new PdfDocumentProcessor();
processor.LoadDocument(stream);
var pages = processor.Document.Pages;
if (pages.Count == 0)
return Task.FromResult<IReadOnlyList<PdfPageImageDto>>(Array.Empty<PdfPageImageDto>());
var renderOptions = PdfPageRenderingParameters.CreateWithResolution(RenderDpi);
var results = new List<PdfPageImageDto>(pages.Count);
for (var i = 0; i < pages.Count; i++)
{
cancellationToken.ThrowIfCancellationRequested();
var pageNumber = i + 1;
var page = pages[i];
var cropBox = page.CropBox;
using DXBitmap bitmap = processor.CreateDXBitmap(pageNumber, renderOptions);
using var imageStream = new MemoryStream();
bitmap.Save(imageStream, DXImageFormat.Png);
var dataUrl = $"data:image/png;base64,{Convert.ToBase64String(imageStream.ToArray())}";
results.Add(new PdfPageImageDto
{
PageNumber = pageNumber,
WidthPt = cropBox.Width,
HeightPt = cropBox.Height,
WidthPx = bitmap.Width,
HeightPx = bitmap.Height,
ImageDataUrl = dataUrl
});
}
return Task.FromResult<IReadOnlyList<PdfPageImageDto>>(results);
}
}

View File

@@ -14,6 +14,7 @@ using EnvelopeGenerator.Infrastructure.Repositories;
using EnvelopeGenerator.Infrastructure.Executor; using EnvelopeGenerator.Infrastructure.Executor;
using EnvelopeGenerator.Application.Common.Interfaces.Repositories; using EnvelopeGenerator.Application.Common.Interfaces.Repositories;
using EnvelopeGenerator.Application.Common.Interfaces.SQLExecutor; using EnvelopeGenerator.Application.Common.Interfaces.SQLExecutor;
using EnvelopeGenerator.Application.Common.Interfaces.Services;
using DigitalData.EmailProfilerDispatcher.Abstraction.Entities; using DigitalData.EmailProfilerDispatcher.Abstraction.Entities;
#elif NETFRAMEWORK #elif NETFRAMEWORK
using System; using System;
@@ -55,6 +56,9 @@ namespace EnvelopeGenerator.Infrastructure
services.TryAddScoped<IEnvelopeTypeRepository, EnvelopeTypeRepository>(); services.TryAddScoped<IEnvelopeTypeRepository, EnvelopeTypeRepository>();
services.TryAddScoped<IReceiverRepository, ReceiverRepository>(); services.TryAddScoped<IReceiverRepository, ReceiverRepository>();
services.TryAddScoped<IEnvelopeReceiverReadOnlyRepository, EnvelopeReceiverReadOnlyRepository>(); services.TryAddScoped<IEnvelopeReceiverReadOnlyRepository, EnvelopeReceiverReadOnlyRepository>();
// User Preferences — distributed cache (dbo.TBDD_CACHE)
services.TryAddScoped<IUserPreferencesService, UserPreferencesService>();
#endif #endif
services.AddDbRepository(opt => services.AddDbRepository(opt =>
@@ -210,4 +214,4 @@ namespace EnvelopeGenerator.Infrastructure
#endif #endif
} }
} }
} }

View File

@@ -204,16 +204,24 @@ public abstract class EGDbContextBase : DbContext
#region Trigger #region Trigger
// Configure entities to handle database triggers // Configure entities to handle database triggers
void AddTrigger<T>() where T : class => _triggers void AddTrigger<T>() where T : class
.Where(t => t.Key == typeof(T).Name) {
.SelectMany(t => t.Value) var entityName = typeof(T).Name;
.ForEach(tName =>
var configuredTriggerNames = _triggers
.Where(t => t.Key == entityName)
.SelectMany(t => t.Value)
.Distinct()
.ToList();
foreach (var triggerName in configuredTriggerNames)
{ {
#if NET #if NET
modelBuilder.Entity<T>().ToTable(tb => tb.HasTrigger(tName)); modelBuilder.Entity<T>().ToTable(tb => tb.HasTrigger(triggerName));
#endif #endif
_logger?.LogInformation("Trigger '{triggerName}' has been added to the '{entityName}' entity.", tName, typeof(T).Name); _logger?.LogInformation("Trigger '{triggerName}' has been added to the '{entityName}' entity.", triggerName, entityName);
}); }
}
// TODO: call add trigger methods with attributes and reflection // TODO: call add trigger methods with attributes and reflection
AddTrigger<Config>(); AddTrigger<Config>();
@@ -243,4 +251,4 @@ public abstract class EGDbContextBase : DbContext
#if NETFRAMEWORK #if NETFRAMEWORK
} }
#endif #endif

View File

@@ -1,48 +0,0 @@
#if NET
using EnvelopeGenerator.Application.Common.Configurations;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Design;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Options;
namespace EnvelopeGenerator.Infrastructure
{
public class EGDbContextFactory : IDesignTimeDbContextFactory<EGDbContext>
{
public EGDbContext CreateDbContext(string[] args)
{
var config = new ConfigurationBuilder()
.SetBasePath(Directory.GetCurrentDirectory())
.AddJsonFile("appsettings.migration.json")
.Build();
// create DbContextOptions
var optionsBuilder = new DbContextOptionsBuilder<EGDbContext>();
optionsBuilder.UseSqlServer(config.GetConnectionString("Default"));
// create DbTriggerParams
var triggerLists = config.GetSection("DbTriggerParams").Get<Dictionary<string, List<string>>>();
var dbTriggerParams = new DbTriggerParams();
if (triggerLists is not null)
foreach (var triggerList in triggerLists)
{
if (triggerList.Value.Count == 0)
continue; // Skip empty trigger lists
var tableName = triggerList.Key;
dbTriggerParams[tableName] = new List<string>();
foreach (var trigger in triggerList.Value)
{
dbTriggerParams[tableName].Add(trigger);
}
}
var dbContext = new EGDbContext(optionsBuilder.Options, Options.Create(dbTriggerParams));
dbContext.IsMigration = true;
return dbContext;
}
}
}
#endif

View File

@@ -26,6 +26,7 @@
<PackageReference Include="DigitalData.Core.Infrastructure" Version="2.6.1" /> <PackageReference Include="DigitalData.Core.Infrastructure" Version="2.6.1" />
<PackageReference Include="HtmlSanitizer" Version="9.0.892" /> <PackageReference Include="HtmlSanitizer" Version="9.0.892" />
<PackageReference Include="Microsoft.Data.SqlClient" Version="5.2.2" /> <PackageReference Include="Microsoft.Data.SqlClient" Version="5.2.2" />
<PackageReference Include="Microsoft.Extensions.Caching.Abstractions" Version="9.0.6" />
<PackageReference Include="Microsoft.Identity.Client" Version="4.82.1" /> <PackageReference Include="Microsoft.Identity.Client" Version="4.82.1" />
<PackageReference Include="QuestPDF" Version="2025.7.1" /> <PackageReference Include="QuestPDF" Version="2025.7.1" />
<PackageReference Include="SixLabors.ImageSharp" Version="3.1.12" /> <PackageReference Include="SixLabors.ImageSharp" Version="3.1.12" />

View File

@@ -0,0 +1,46 @@
#if NET
using System.Text.Json;
using Microsoft.Extensions.Caching.Distributed;
using EnvelopeGenerator.Application.Common.Dto;
using EnvelopeGenerator.Application.Common.Interfaces.Services;
namespace EnvelopeGenerator.Infrastructure;
/// <summary>
/// Implements user preferences storage using the distributed cache (dbo.TBDD_CACHE).
/// No DB schema changes required — uses the existing SQL Server distributed cache table.
/// Cache key schema: "signflow.user-preferences:{username}"
/// Expiration: indefinite (no TTL) — preferences persist until explicitly overwritten.
/// </summary>
public sealed class UserPreferencesService(IDistributedCache cache) : IUserPreferencesService
{
private const string KeyPrefix = "signflow.user-preferences:";
private static string BuildKey(string id) => $"{KeyPrefix}{id}";
/// <inheritdoc />
public async Task<UserPreferencesDto> GetOrCreateAsync(string id, CancellationToken ct = default)
{
var json = await cache.GetStringAsync(BuildKey(id), ct);
if(json is not null && JsonSerializer.Deserialize<UserPreferencesDto>(json) is UserPreferencesDto pref)
{
return pref;
}
else
{
// init new
await SaveAsync(id, UserPreferencesDto.Default, ct);
return UserPreferencesDto.Default;
}
}
/// <inheritdoc />
public async Task SaveAsync(string id, UserPreferencesDto preferences, CancellationToken ct = default)
{
var json = JsonSerializer.Serialize(preferences);
// No expiration — preferences are stored indefinitely.
await cache.SetStringAsync(BuildKey(id), json, new DistributedCacheEntryOptions(), ct);
}
}
#endif

View File

@@ -1,10 +0,0 @@
<Router AppAssembly="@typeof(Program).Assembly">
<Found Context="routeData">
<RouteView RouteData="@routeData" DefaultLayout="@typeof(MainLayout)" />
</Found>
<NotFound>
<LayoutView Layout="@typeof(MainLayout)">
<p>Sorry, there's nothing at this address.</p>
</LayoutView>
</NotFound>
</Router>

View File

@@ -1,51 +0,0 @@
<Project Sdk="Microsoft.NET.Sdk.BlazorWebAssembly">
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<WasmBuildNative>true</WasmBuildNative>
<InvariantGlobalization>false</InvariantGlobalization>
<BlazorWebAssemblyLoadAllGlobalizationData>true</BlazorWebAssemblyLoadAllGlobalizationData>
<Nullable>enable</Nullable>
<PackageId>EnvelopeGenerator.ReceiverUI</PackageId>
<Authors>Digital Data GmbH</Authors>
<Company>Digital Data GmbH</Company>
<Product>EnvelopeGenerator.ReceiverUI</Product>
<PackageIcon>Assets\icon.ico</PackageIcon>
<PackageTags>digital data envelope generator web</PackageTags>
<Description>EnvelopeGenerator.ReceiverUI is a Blazor WebAssembly application developed to manage signing processes. It uses Entity Framework Core (EF Core) for database operations. The user interface for signing processes is developed with Razor View Engine (.cshtml files) and JavaScript under wwwroot, integrated with PSPDFKit. This integration allows users to view and sign documents seamlessly.</Description>
<Version>1.4.1</Version>
<!-- NuGet package version -->
<AssemblyVersion>1.4.1.0</AssemblyVersion>
<!-- Assembly version for API compatibility -->
<FileVersion>1.4.1.0</FileVersion>
<!-- Windows file version -->
<Copyright>Copyright © 2026 Digital Data GmbH. All rights reserved.</Copyright>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="DevExpress.Blazor.PdfViewer" Version="25.2.3" />
<PackageReference Include="DevExpress.Blazor.Reporting.JSBasedControls" Version="25.2.3" />
<PackageReference Include="DevExpress.Blazor.Reporting.Viewer" Version="25.2.3" />
<PackageReference Include="DevExpress.Drawing.Skia" Version="25.2.3" />
<PackageReference Include="HarfBuzzSharp.NativeAssets.WebAssembly" Version="8.3.1.2" />
<PackageReference Include="SkiaSharp.NativeAssets.WebAssembly" Version="3.119.1" />
<PackageReference Include="SkiaSharp.Views.Blazor" Version="3.119.1" />
<NativeFileReference Include="$(HarfBuzzSharpStaticLibraryPath)\2.0.23\*.a" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly" Version="8.0.11" />
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly.DevServer" Version="8.0.11" PrivateAssets="all" />
</ItemGroup>
<ItemGroup>
<Folder Include="Properties\PublishProfiles\" />
</ItemGroup>
<ItemGroup>
<Content Update="wwwroot\docs\privacy-policy.en-US.html">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</Content>
<Content Update="wwwroot\docs\privacy-policy.fr-FR.html">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</Content>
</ItemGroup>
</Project>

View File

@@ -1,12 +0,0 @@
The following open source libraries are used and included within thеse sample/demonstration projects:
Bootstrap (Open Source – MIT License)
Copyright (c) 2011-2021 Twitter, Inc. / Copyright (c) 2011-2021 The Bootstrap Authors
https://github.com/twbs/bootstrap/blob/main/LICENSE
open-iconic (Open Source – MIT License and SIL Open Font License)
Copyright (c) 2014 Waybury
https://github.com/iconic/open-iconic/blob/master/ICON-LICENSE
https://github.com/iconic/open-iconic/blob/master/FONT-LICENSE
The open source libraries included in these sample/demonstration projects are done so pursuant to each individual open source library license and subject to the disclaimers and limitations on liability set forth in each open source library license.

View File

@@ -1,10 +0,0 @@
namespace EnvelopeGenerator.ReceiverUI.Options;
public class ApiOptions
{
public const string SectionName = "Api";
public string BaseUrl { get; set; } = string.Empty;
public bool ForceToUseFakeDocument { get; set; } = false;
}

View File

@@ -1,5 +0,0 @@
@page "/documentviewer"
<DxDocumentViewer ReportName="LargeDatasetReport" CssClass="dx-blazor-reporting-container" Height="@null" Width="@null">
<DxDocumentViewerTabPanelSettings Width="340" />
</DxDocumentViewer>

File diff suppressed because it is too large Load Diff

View File

@@ -1,71 +0,0 @@
@page "/"
@inject IJSRuntime JS
<link href="_content/DevExpress.Blazor.Themes/blazing-berry.bs5.min.css" rel="stylesheet" />
<div class="home-page-wrapper">
<div class="home-hero-header">
<div class="home-hero-header__inner">
<svg xmlns="http://www.w3.org/2000/svg" width="48" height="48" fill="currentColor" class="home-hero-header__icon" viewBox="0 0 16 16">
<path d="M0 4a2 2 0 0 1 2-2h12a2 2 0 0 1 2 2v8a2 2 0 0 1-2 2H2a2 2 0 0 1-2-2V4Zm2-1a1 1 0 0 0-1 1v.217l7 4.2 7-4.2V4a1 1 0 0 0-1-1H2Zm13 2.383-4.708 2.825L15 11.105V5.383Zm-.034 6.876-5.64-3.471L8 9.583l-1.326-.795-5.64 3.47A1 1 0 0 0 2 13h12a1 1 0 0 0 .966-.741ZM1 11.105l4.708-2.897L1 5.383v5.722Z"/>
</svg>
<div>
<h1 class="home-hero-header__title">SignFlow</h1>
<p class="home-hero-header__subtitle">Willkommen im eSign-Portal</p>
</div>
</div>
</div>
<div class="home-content">
<div class="home-card card shadow border-0">
<div class="card-body p-4 p-md-5">
<p class="text-muted mb-4" style="font-size: 0.92rem; line-height: 1.7; text-align: justify; text-align-last: left; min-height: calc(0.92rem * 1.7 * 9);">
<span id="home-description"></span>
</p>
<div class="mt-4 pt-3 border-top">
<div class="d-flex flex-wrap justify-content-center gap-3">
<div class="home-feature-badge">
<svg xmlns="http://www.w3.org/2000/svg" width="15" height="15" fill="currentColor" class="me-1" viewBox="0 0 16 16">
<path d="M8 1a2 2 0 0 1 2 2v4H6V3a2 2 0 0 1 2-2zm3 6V3a3 3 0 0 0-6 0v4a2 2 0 0 0-2 2v5a2 2 0 0 0 2 2h6a2 2 0 0 0 2-2V9a2 2 0 0 0-2-2z"/>
</svg>
Sicherer Zugang
</div>
<div class="home-feature-badge">
<svg xmlns="http://www.w3.org/2000/svg" width="15" height="15" fill="currentColor" class="me-1" viewBox="0 0 16 16">
<path d="M13.854 3.646a.5.5 0 0 1 0 .708l-7 7a.5.5 0 0 1-.708 0l-3.5-3.5a.5.5 0 1 1 .708-.708L6.5 10.293l6.646-6.647a.5.5 0 0 1 .708 0z"/>
</svg>
Digitale Unterschrift
</div>
<div class="home-feature-badge">
<svg xmlns="http://www.w3.org/2000/svg" width="15" height="15" fill="currentColor" class="me-1" viewBox="0 0 16 16">
<path d="M.5 9.9a.5.5 0 0 1 .5.5v2.5a1 1 0 0 0 1 1h12a1 1 0 0 0 1-1v-2.5a.5.5 0 0 1 1 0v2.5a2 2 0 0 1-2 2H2a2 2 0 0 1-2-2v-2.5a.5.5 0 0 1 .5-.5z"/>
<path d="M7.646 11.854a.5.5 0 0 0 .708 0l3-3a.5.5 0 0 0-.708-.708L8.5 10.293V1.5a.5.5 0 0 0-1 0v8.793L5.354 8.146a.5.5 0 1 0-.708.708l3 3z"/>
</svg>
PDF-Export
</div>
</div>
</div>
</div>
</div>
</div>
</div>
@code {
private const string HomePageDescription =
"Das digitale Unterschriftenportal ist eine Plattform, die entwickelt wurde, um Ihre Dokumente sicher zu unterschreiben und zu verwalten. " +
"Mit seiner benutzerfreundlichen Oberfläche können Sie Ihre Dokumente schnell hochladen, die Unterschriftsprozesse verfolgen und Ihre digitalen Unterschriftenanwendungen einfach durchführen. " +
"Dieses Portal beschleunigt Ihren Arbeitsablauf mit rechtlich gültigen Unterschriften und erhöht gleichzeitig die Sicherheit Ihrer Dokumente.";
protected override async Task OnAfterRenderAsync(bool firstRender)
{
if (firstRender)
{
await JS.InvokeVoidAsync("receiverSignature.startTyped", "home-description", HomePageDescription, 15);
}
}
}

View File

@@ -1,19 +0,0 @@
@page "/sender"
@using DevExpress.DataAccess.Json;
@using EnvelopeGenerator.ReceiverUI.Services;
<DxReportDesigner ReportName="LargeDatasetReport" CssClass="dx-blazor-reporting-container" Height="@null" Width="@null" AllowMDI="true" DataSources="DataSources">
<DxReportDesignerWizardSettings UseFullscreenWizard="true" />
</DxReportDesigner>
@code {
Dictionary<string, object> DataSources = new Dictionary<string, object>();
protected override async Task OnInitializedAsync() {
await base.OnInitializedAsync();
var connection = CustomDataSourceWizardJsonDataConnectionStorage.GetDefaultConnection();
JsonDataSource jsonDataSource = new JsonDataSource();
jsonDataSource.JsonSource = connection.GetJsonSource();
await jsonDataSource.FillAsync();
DataSources.Add("Northwind", jsonDataSource);
}
}

View File

@@ -1,735 +0,0 @@
@page "/receiver/{EnvelopeKey}"
@page "/report-viewer/{EnvelopeKey}"
@using System.Drawing
@using DevExpress.Blazor
@using DevExpress.Drawing
@using DevExpress.Utils
@using DevExpress.XtraPrinting
@using DevExpress.XtraPrinting.Drawing
@using Microsoft.JSInterop
@using XtraReport = DevExpress.XtraReports.UI.XtraReport
@using BottomMarginBand = DevExpress.XtraReports.UI.BottomMarginBand
@using XRLabel = DevExpress.XtraReports.UI.XRLabel
@using XRPictureBox = DevExpress.XtraReports.UI.XRPictureBox
@using XRControl = DevExpress.XtraReports.UI.XRControl
@using ImageSizeMode = DevExpress.XtraPrinting.ImageSizeMode
@using EnvelopeGenerator.ReceiverUI.Services
@using DevExpress.Blazor.Reporting
@using Microsoft.Extensions.Options
@using EnvelopeGenerator.ReceiverUI.Options
@using EnvelopeGenerator.ReceiverUI.Models
@implements IDisposable
@inject IJSRuntime JSRuntime
@inject AnnotationService AnnotationService
@inject IOptions<ApiOptions> AppOptions
@inject NavigationManager Navigation
@inject InMemoryReportStorageWebExtension ReportStorage
@inject EnvelopeGenerator.ReceiverUI.Services.DocumentService DocumentService
@inject EnvelopeGenerator.ReceiverUI.Services.AuthService AuthService
@inject EnvelopeGenerator.ReceiverUI.Services.EnvelopeReceiverService EnvelopeReceiverService
<link href="_content/DevExpress.Blazor.Themes/blazing-berry.bs5.min.css" rel="stylesheet" />
<link href="_content/DevExpress.Blazor.Reporting.Viewer/css/dx-blazor-reporting-components.bs5.css" rel="stylesheet" />
<div class="receiver-page-layout">
<div class="receiver-signature-panel">
@* ?? Envelope info header ???????????????????????????????????????????????? *@
@if (_envelopeReceiver is not null) {
<div class="receiver-info-header">
<div class="receiver-info-header__gradient">
<div class="receiver-info-header__left">
<svg xmlns="http://www.w3.org/2000/svg" width="22" height="22" fill="currentColor" class="receiver-info-header__icon" viewBox="0 0 16 16">
<path d="M0 4a2 2 0 0 1 2-2h12a2 2 0 0 1 2 2v8a2 2 0 0 1-2 2H2a2 2 0 0 1-2-2V4Zm2-1a1 1 0 0 0-1 1v.217l7 4.2 7-4.2V4a1 1 0 0 0-1-1H2Zm13 2.383-4.708 2.825L15 11.105V5.383Zm-.034 6.876-5.64-3.471L8 9.583l-1.326-.795-5.64 3.47A1 1 0 0 0 2 13h12a1 1 0 0 0 .966-.741ZM1 11.105l4.708-2.897L1 5.383v5.722Z"/>
</svg>
<div>
<div class="receiver-info-header__title">@(_envelopeReceiver.Envelope?.Title ?? "Dokument")</div>
@if (!string.IsNullOrWhiteSpace(_envelopeReceiver.Envelope?.User?.FullName) || !string.IsNullOrWhiteSpace(_envelopeReceiver.Envelope?.User?.Email)) {
<div class="receiver-info-header__sender">
Von
@if (!string.IsNullOrWhiteSpace(_envelopeReceiver.Envelope?.User?.FullName)) {
<strong>@_envelopeReceiver.Envelope!.User!.FullName</strong>
}
@if (!string.IsNullOrWhiteSpace(_envelopeReceiver.Envelope?.User?.Email)) {
<span class="opacity-75">&lt;@_envelopeReceiver.Envelope!.User!.Email&gt;</span>
}
&nbsp;&middot;&nbsp; @_envelopeReceiver.Envelope?.AddedWhen.ToString("dd.MM.yyyy")
</div>
}
</div>
</div>
<div class="receiver-info-header__badges">
@if (!string.IsNullOrWhiteSpace(_envelopeReceiver.Name)) {
<span class="receiver-info-badge">
<svg xmlns="http://www.w3.org/2000/svg" width="12" height="12" fill="currentColor" class="me-1" viewBox="0 0 16 16">
<path d="M8 8a3 3 0 1 0 0-6 3 3 0 0 0 0 6Zm2-3a2 2 0 1 1-4 0 2 2 0 0 1 4 0Zm4 8c0 1-1 1-1 1H3s-1 0-1-1 1-4 6-4 6 3 6 4Z"/>
</svg>
@_envelopeReceiver.Name
</span>
}
@if (!string.IsNullOrWhiteSpace(_envelopeReceiver.CompanyName)) {
<span class="receiver-info-badge">
<svg xmlns="http://www.w3.org/2000/svg" width="12" height="12" fill="currentColor" class="me-1" viewBox="0 0 16 16">
<path d="M14.763.075A.5.5 0 0 1 15 .5v15a.5.5 0 0 1-.5.5h-3a.5.5 0 0 1-.5-.5V14h-1v1.5a.5.5 0 0 1-.5.5h-9a.5.5 0 0 1-.5-.5V10a.5.5 0 0 1 .342-.474L6 7.64V4.5a.5.5 0 0 1 .276-.447l8-4a.5.5 0 0 1 .487.022Z"/>
</svg>
@_envelopeReceiver.CompanyName
</span>
}
@if (!string.IsNullOrWhiteSpace(_envelopeReceiver.JobTitle)) {
<span class="receiver-info-badge receiver-info-badge--muted">@_envelopeReceiver.JobTitle</span>
}
@{
var docElements = _envelopeReceiver.Envelope?.Documents?.FirstOrDefault()?.Elements;
int sigCount = docElements?.Count() ?? _annotations.Count;
}
@if (sigCount > 0) {
<span class="receiver-info-badge receiver-info-badge--accent">
<svg xmlns="http://www.w3.org/2000/svg" width="12" height="12" fill="currentColor" class="me-1" viewBox="0 0 16 16">
<path d="M12.146.146a.5.5 0 0 1 .708 0l3 3a.5.5 0 0 1 0 .708l-10 10a.5.5 0 0 1-.168.11l-5 2a.5.5 0 0 1-.65-.65l2-5a.5.5 0 0 1 .11-.168l10-10zM11.207 2.5 13.5 4.793 14.793 3.5 12.5 1.207 11.207 2.5zm1.586 3L10.5 3.207 4 9.707V10h.5a.5.5 0 0 1 .5.5v.5h.5a.5.5 0 0 1 .5.5v.5h.293l6.5-6.5zm-9.761 5.175-.106.106-1.528 3.821 3.821-1.528.106-.106A.5.5 0 0 1 5 12.5V12h-.5a.5.5 0 0 1-.5-.5V11h-.5a.5.5 0 0 1-.468-.325z"/>
</svg>
@sigCount @(sigCount == 1 ? "Unterschriftsfeld" : "Unterschriftsfelder")
</span>
}
</div>
</div>
@if (!string.IsNullOrWhiteSpace(_envelopeReceiver.Envelope?.Message)) {
<div class="receiver-info-message">@_envelopeReceiver.Envelope!.Message</div>
}
@if (!string.IsNullOrWhiteSpace(_envelopeReceiver.PrivateMessage)) {
<div class="receiver-info-private-message">
<svg xmlns="http://www.w3.org/2000/svg" width="13" height="13" fill="currentColor" class="me-1 flex-shrink-0" viewBox="0 0 16 16">
<path d="M8 1a2 2 0 0 1 2 2v4H6V3a2 2 0 0 1 2-2zm3 6V3a3 3 0 0 0-6 0v4a2 2 0 0 0-2 2v5a2 2 0 0 0 2 2h6a2 2 0 0 0 2-2V9a2 2 0 0 0-2-2z"/>
</svg>
@_envelopeReceiver.PrivateMessage
</div>
}
</div>
}
@* ?? Signature action bar ???????????????????????????????????????????????? *@
<div class="receiver-action-bar">
<div class="receiver-action-bar__inner">
<button class="btn btn-sm @(_capturedSignature is not null ? "btn-outline-success" : "btn-primary")" @onclick="OpenSignaturePopupAsync">
@if (_capturedSignature is not null) {
<svg xmlns="http://www.w3.org/2000/svg" width="13" height="13" fill="currentColor" class="me-1" viewBox="0 0 16 16">
<path d="M13.854 3.646a.5.5 0 0 1 0 .708l-7 7a.5.5 0 0 1-.708 0l-3.5-3.5a.5.5 0 1 1 .708-.708L6.5 10.293l6.646-6.647a.5.5 0 0 1 .708 0z"/>
</svg>
<span>Unterschrift gespeichert</span>
} else {
<svg xmlns="http://www.w3.org/2000/svg" width="13" height="13" fill="currentColor" class="me-1" viewBox="0 0 16 16">
<path d="M12.146.146a.5.5 0 0 1 .708 0l3 3a.5.5 0 0 1 0 .708l-10 10a.5.5 0 0 1-.168.11l-5 2a.5.5 0 0 1-.65-.65l2-5a.5.5 0 0 1 .11-.168l10-10zM11.207 2.5 13.5 4.793 14.793 3.5 12.5 1.207 11.207 2.5zm1.586 3L10.5 3.207 4 9.707V10h.5a.5.5 0 0 1 .5.5v.5h.5a.5.5 0 0 1 .5.5v.5h.293l6.5-6.5zm-9.761 5.175-.106.106-1.528 3.821 3.821-1.528.106-.106A.5.5 0 0 1 5 12.5V12h-.5a.5.5 0 0 1-.5-.5V11h-.5a.5.5 0 0 1-.468-.325z"/>
</svg>
<span>Unterschrift erstellen</span>
}
</button>
@if (_annotations.Count > 0 && !SignatureApplied) {
<div class="receiver-action-bar__progress">
<div class="progress" style="height:5px; min-width:70px; width:90px;">
<div class="progress-bar @(_checkedAnnotations.Count == _annotations.Count ? "bg-success" : "bg-primary")"
style="width:@(_annotations.Count > 0 ? (_checkedAnnotations.Count * 100 / _annotations.Count) : 0)%"
role="progressbar"></div>
</div>
<span class="text-muted" style="font-size:0.75rem;">
@_checkedAnnotations.Count&thinsp;/&thinsp;@_annotations.Count
Seite@(AnnotationPages.Count() == 1 ? "" : "n")&nbsp;@string.Join(",", AnnotationPages)
</span>
@if (_capturedSignature is null) {
<span class="text-muted fst-italic" style="font-size:0.72rem;">Zuerst Unterschrift erstellen</span>
} else if (_checkedAnnotations.Count == _annotations.Count) {
<span class="text-success fw-semibold" style="font-size:0.72rem;">&#10003; Wird angewendet&hellip;</span>
}
</div>
}
@if (!string.IsNullOrWhiteSpace(SignatureValidationMessage)) {
<span class="text-danger" style="font-size:0.78rem;">@SignatureValidationMessage</span>
}
@if (SignatureApplied) {
<span class="text-success" style="font-size:0.78rem;">
<svg xmlns="http://www.w3.org/2000/svg" width="13" height="13" fill="currentColor" class="me-1" viewBox="0 0 16 16">
<path d="M16 8A8 8 0 1 1 0 8a8 8 0 0 1 16 0zm-3.97-3.03a.75.75 0 0 0-1.08.022L7.477 9.417 5.384 7.323a.75.75 0 0 0-1.06 1.06L6.97 11.03a.75.75 0 0 0 1.079-.02l3.992-4.99a.75.75 0 0 0-.01-1.05z"/>
</svg>
Unterschrift angewendet
</span>
}
<div class="ms-auto d-flex align-items-center gap-2">
<button class="btn btn-sm btn-success" disabled="@(!SignatureApplied)" @onclick="ExportSignedPdfAsync">
<svg xmlns="http://www.w3.org/2000/svg" width="13" height="13" fill="currentColor" class="me-1" viewBox="0 0 16 16">
<path d="M.5 9.9a.5.5 0 0 1 .5.5v2.5a1 1 0 0 0 1 1h12a1 1 0 0 0 1-1v-2.5a.5.5 0 0 1 1 0v2.5a2 2 0 0 1-2 2H2a2 2 0 0 1-2-2v-2.5a.5.5 0 0 1 .5-.5z"/>
<path d="M7.646 11.854a.5.5 0 0 0 .708 0l3-3a.5.5 0 0 0-.708-.708L8.5 10.293V1.5a.5.5 0 0 0-1 0v8.793L5.354 8.146a.5.5 0 1 0-.708.708l3 3z"/>
</svg>
PDF exportieren
</button>
@if (!string.IsNullOrWhiteSpace(EnvelopeKey)) {
<button class="btn btn-sm btn-outline-danger" @onclick="LogoutAsync" disabled="@IsLoggingOut" title="Abmelden">
@if (IsLoggingOut) {
<span class="spinner-border spinner-border-sm" role="status" aria-hidden="true"></span>
} else {
<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" fill="currentColor" viewBox="0 0 16 16">
<path fill-rule="evenodd" d="M10 12.5a.5.5 0 0 1-.5.5h-8a.5.5 0 0 1-.5-.5v-9a.5.5 0 0 1 .5-.5h8a.5.5 0 0 1 .5.5v2a.5.5 0 0 0 1 0v-2A1.5 1.5 0 0 0 9.5 2h-8A1.5 1.5 0 0 0 0 3.5v9A1.5 1.5 0 0 0 1.5 14h8a1.5 1.5 0 0 0 1.5-1.5v-2a.5.5 0 0 0-1 0v2z"/>
<path fill-rule="evenodd" d="M15.854 8.354a.5.5 0 0 0 0-.708l-3-3a.5.5 0 0 0-.708.708L14.293 7.5H5.5a.5.5 0 0 0 0 1h8.793l-2.147 2.146a.5.5 0 0 0 .708.708l3-3z"/>
</svg>
}
</button>
}
</div>
</div>
</div>
</div>
<DxPopup @bind-Visible="SignaturePopupVisible"
HeaderText="Unterschrift erfassen"
Width="620px"
ShowFooter="true"
CloseOnEscape="false"
ShowCloseButton="false"
CloseOnOutsideClick="false"
Shown="OnPopupShownAsync">
<BodyContentTemplate>
<ul class="nav nav-tabs mb-3">
<li class="nav-item">
<button type="button" class="nav-link @(ActiveSignatureTab == SignatureTabDraw ? "active" : null)" @onclick="() => SetSignatureTabAsync(SignatureTabDraw)">Zeichnen</button>
</li>
<li class="nav-item">
<button type="button" class="nav-link @(ActiveSignatureTab == SignatureTabText ? "active" : null)" @onclick="() => SetSignatureTabAsync(SignatureTabText)">Text</button>
</li>
<li class="nav-item">
<button type="button" class="nav-link @(ActiveSignatureTab == SignatureTabImage ? "active" : null)" @onclick="() => SetSignatureTabAsync(SignatureTabImage)">Bild</button>
</li>
</ul>
@if(ActiveSignatureTab == SignatureTabDraw) {
<p class="text-muted mb-2">Bitte unterschreiben Sie im folgenden Feld.</p>
<canvas id="receiver-signature-pad" width="520" height="160" class="border rounded bg-white w-100" style="max-width: 520px; touch-action: none;"></canvas>
} else if(ActiveSignatureTab == SignatureTabText) {
<p class="text-muted mb-2">Geben Sie Ihre Unterschrift als Text ein und waehlen Sie eine Schriftart.</p>
<div class="row g-2 mb-2">
<div class="col-12 col-md-7">
<input class="form-control" placeholder="Ihre Unterschrift" value="@TypedSignatureText" @oninput="OnTypedSignatureChanged" />
</div>
<div class="col-12 col-md-5">
<select class="form-select" value="@TypedSignatureFont" @onchange="OnTypedSignatureFontChanged">
@foreach(var font in TypedSignatureFonts) {
<option value="@font.Value" style="font-family: @font.Value">@font.Text</option>
}
</select>
</div>
</div>
<canvas id="receiver-typed-signature-pad" width="520" height="160" class="border rounded bg-white w-100" style="max-width: 520px;"></canvas>
} else {
<p class="text-muted mb-2">Laden Sie ein Bild Ihrer Unterschrift hoch.</p>
<input id="receiver-signature-image-input" class="form-control mb-2" type="file" accept="image/png,image/jpeg,image/webp" />
<canvas id="receiver-image-signature-pad" width="520" height="160" class="border rounded bg-white w-100" style="max-width: 520px;"></canvas>
}
<div class="border-top mt-3 pt-3">
<p class="text-muted mb-2">Bitte geben Sie die folgenden Angaben ein. Das Datum wird automatisch hinzugefuegt.</p>
<div class="row g-2">
<div class="col-12 col-md-6">
<label class="form-label" for="receiver-signer-name">Vor- und Nachname *</label>
<input id="receiver-signer-name" class="form-control" value="@SignerFullName" @oninput="args => SignerFullName = args.Value?.ToString() ?? string.Empty" />
</div>
<div class="col-12 col-md-6">
<label class="form-label" for="receiver-signer-position">Position</label>
<input id="receiver-signer-position" class="form-control" value="@SignerPosition" @oninput="args => SignerPosition = args.Value?.ToString() ?? string.Empty" />
</div>
<div class="col-12 col-md-6">
<label class="form-label" for="receiver-signature-place">Ort *</label>
<input id="receiver-signature-place" class="form-control" value="@SignaturePlace" @oninput="args => SignaturePlace = args.Value?.ToString() ?? string.Empty" />
</div>
</div>
</div>
@if(!string.IsNullOrWhiteSpace(PopupValidationMessage)) {
<div class="text-danger mt-2">@PopupValidationMessage</div>
}
</BodyContentTemplate>
<FooterContentTemplate>
<div class="d-flex gap-2 flex-wrap justify-content-end w-100">
<button class="btn btn-outline-secondary" @onclick="RenewSignatureAsync">Unterschrift erneuern</button>
<button class="btn btn-primary" @onclick="SaveSignatureAsync">Speichern</button>
</div>
</FooterContentTemplate>
</DxPopup>
<div class="receiver-viewer-wrapper">
@if(Report is not null) {
<DxReportViewer @key="ViewerKey" @ref="reportViewer" Report="Report" RootCssClasses="w-100 h-100" Zoom="1.3" />
}
</div>
</div>
@code {
const string SignatureTabDraw = "draw";
const string SignatureTabText = "text";
const string SignatureTabImage = "image";
const string DrawCanvasId = "receiver-signature-pad";
const string TypedCanvasId = "receiver-typed-signature-pad";
const string ImageInputId = "receiver-signature-image-input";
const string ImageCanvasId = "receiver-image-signature-pad";
readonly (string Text, string Value)[] TypedSignatureFonts = {
("Brush Script", "'Brush Script MT', cursive"),
("Segoe Script", "'Segoe Script', cursive"),
("Lucida Handwriting", "'Lucida Handwriting', cursive"),
("Comic Sans", "'Comic Sans MS', cursive"),
("Cursive", "cursive")
};
[Parameter] public string EnvelopeKey { get; set; } = string.Empty;
DxReportViewer? reportViewer;
XtraReport? Report;
bool SignatureApplied;
bool SignaturePopupVisible;
string? SignatureValidationMessage;
string? PopupValidationMessage;
string ActiveSignatureTab = SignatureTabDraw;
string TypedSignatureText = string.Empty;
string TypedSignatureFont = "'Brush Script MT', cursive";
string SignerFullName = string.Empty;
string SignerPosition = string.Empty;
string SignaturePlace = string.Empty;
int ViewerKey;
bool IsLoggingOut;
IReadOnlyList<AnnotationDto> _annotations = [];
IEnumerable<int> AnnotationPages => _annotations.Select(a => a.Page).Distinct().OrderBy(p => p);
EnvelopeReceiverDto? _envelopeReceiver;
record SignatureCapture(string DataUrl, string FullName, string Position, string Place);
SignatureCapture? _capturedSignature;
byte[]? _basePdfBytes;
// annotation IDs the user has checked via overlay checkboxes
readonly HashSet<long> _checkedAnnotations = [];
DotNetObjectReference<ReportViewer>? _dotNetRef;
int _lastOverlayViewerKey = -1;
async Task LogoutAsync() {
if (string.IsNullOrWhiteSpace(EnvelopeKey) || IsLoggingOut) return;
IsLoggingOut = true;
await InvokeAsync(StateHasChanged);
await AuthService.LogoutEnvelopeReceiverAsync(EnvelopeKey);
Navigation.NavigateTo($"/login/{Uri.EscapeDataString(EnvelopeKey)}", forceLoad: true);
}
protected override async Task OnInitializedAsync() {
// ? REDIRECT: /receiver/{key} -> /envelope/{key} (NEW PDF.js viewer)
if (!string.IsNullOrWhiteSpace(EnvelopeKey)) {
Navigation.NavigateTo($"/envelope/{Uri.EscapeDataString(EnvelopeKey)}", forceLoad: false);
return;
}
if (!string.IsNullOrWhiteSpace(EnvelopeKey)) {
var hasAccess = await AuthService.CheckEnvelopeAccessAsync(EnvelopeKey);
if (!hasAccess) {
Navigation.NavigateTo($"/login/{Uri.EscapeDataString(EnvelopeKey)}");
return;
}
else
{
ActiveSignatureTab = SignatureTabDraw;
SignaturePopupVisible = true;
SignatureValidationMessage = null;
PopupValidationMessage = null;
}
}
_annotations = await AnnotationService.GetAnnotationsAsync(EnvelopeKey ?? "fake");
_envelopeReceiver = await EnvelopeReceiverService.GetAsync(EnvelopeKey ?? "fake");
if (!AppOptions.Value.ForceToUseFakeDocument && !string.IsNullOrWhiteSpace(EnvelopeKey)) {
var (pdfBytes, _) = await DocumentService.GetDocumentAsync(EnvelopeKey);
if (pdfBytes is { Length: > 0 })
_basePdfBytes = pdfBytes;
}
var initialReport = BuildFreshBaseReport();
Report = initialReport;
}
protected override async Task OnAfterRenderAsync(bool firstRender) {
if (firstRender)
_dotNetRef = DotNetObjectReference.Create(this);
if (Report is not null && _annotations.Count > 0
&& _capturedSignature is not null && !SignatureApplied
&& _lastOverlayViewerKey != ViewerKey) {
_lastOverlayViewerKey = ViewerKey;
await JSRuntime.InvokeVoidAsync(
"receiverSignature.installAnnotationCheckboxes",
_annotations, _checkedAnnotations.ToArray(), _dotNetRef);
}
}
[JSInvokable]
public async Task OnAnnotationToggled(long annotationId, bool isChecked) {
if (isChecked)
_checkedAnnotations.Add(annotationId);
else
_checkedAnnotations.Remove(annotationId);
await InvokeAsync(StateHasChanged);
if (_capturedSignature is not null
&& !SignatureApplied
&& _annotations.Count > 0
&& _checkedAnnotations.Count == _annotations.Count) {
// K?sa bekleme: kullan?c? son tick'in görsel feedback'ini görsün
await Task.Delay(400);
await SubmitSignaturesAsync();
}
}
void OpenSignaturePopupAsync() {
ActiveSignatureTab = SignatureTabDraw;
SignaturePopupVisible = true;
SignatureValidationMessage = null;
PopupValidationMessage = null;
}
async Task OnPopupShownAsync() {
await InitializeActiveSignatureTabAsync();
}
async Task SetSignatureTabAsync(string tab) {
ActiveSignatureTab = tab;
PopupValidationMessage = null;
await InvokeAsync(StateHasChanged);
await Task.Delay(50);
await InitializeActiveSignatureTabAsync();
}
async Task InitializeActiveSignatureTabAsync() {
if(ActiveSignatureTab == SignatureTabDraw) {
await JSRuntime.InvokeVoidAsync("receiverSignature.initialize", DrawCanvasId);
} else if(ActiveSignatureTab == SignatureTabText) {
await JSRuntime.InvokeVoidAsync("receiverSignature.initializeTyped", TypedCanvasId);
await RenderTypedSignatureAsync();
} else {
await JSRuntime.InvokeVoidAsync("receiverSignature.initializeImage", ImageInputId, ImageCanvasId);
}
}
async Task RenewSignatureAsync() {
PopupValidationMessage = null;
if(ActiveSignatureTab == SignatureTabDraw) {
await JSRuntime.InvokeVoidAsync("receiverSignature.clear", DrawCanvasId);
} else if(ActiveSignatureTab == SignatureTabText) {
TypedSignatureText = string.Empty;
await JSRuntime.InvokeVoidAsync("receiverSignature.clearTyped", TypedCanvasId);
} else {
await JSRuntime.InvokeVoidAsync("receiverSignature.clearImage", ImageInputId, ImageCanvasId);
}
}
void CloseSignaturePopup() {
PopupValidationMessage = null;
SignaturePopupVisible = false;
}
async Task OnTypedSignatureChanged(Microsoft.AspNetCore.Components.ChangeEventArgs args) {
TypedSignatureText = args.Value?.ToString() ?? string.Empty;
await RenderTypedSignatureAsync();
}
async Task OnTypedSignatureFontChanged(Microsoft.AspNetCore.Components.ChangeEventArgs args) {
TypedSignatureFont = args.Value?.ToString() ?? TypedSignatureFont;
await RenderTypedSignatureAsync();
}
async Task RenderTypedSignatureAsync() {
await JSRuntime.InvokeVoidAsync("receiverSignature.renderTypedSignature", TypedCanvasId, TypedSignatureText, TypedSignatureFont);
}
async Task SaveSignatureAsync() {
if (string.IsNullOrWhiteSpace(SignerFullName)) {
PopupValidationMessage = "Bitte geben Sie Vor- und Nachname ein.";
return;
}
if (string.IsNullOrWhiteSpace(SignaturePlace)) {
PopupValidationMessage = "Bitte geben Sie den Ort ein.";
return;
}
var signatureDataUrl = await GetActiveSignatureDataUrlAsync();
if (string.IsNullOrWhiteSpace(signatureDataUrl)) {
PopupValidationMessage = "Die Unterschrift ist fuer den PDF-Export erforderlich.";
return;
}
PopupValidationMessage = null;
SignatureValidationMessage = null;
_capturedSignature = new(signatureDataUrl, SignerFullName.Trim(), SignerPosition.Trim(), SignaturePlace.Trim());
// If no annotations, apply immediately (no checkbox step needed)
if (_annotations.Count == 0) {
var freshReport = BuildFreshBaseReport();
AddSignature(freshReport, _capturedSignature.DataUrl, _capturedSignature.FullName, _capturedSignature.Position, _capturedSignature.Place);
Report = freshReport;
SignatureApplied = true;
SignaturePopupVisible = false;
ViewerKey++;
return;
}
// Close popup; checkboxes will appear on the PDF via OnAfterRenderAsync
SignaturePopupVisible = false;
_lastOverlayViewerKey = -1; // force overlay reinstall
await InvokeAsync(StateHasChanged);
}
async Task SubmitSignaturesAsync() {
if (_checkedAnnotations.Count == 0) {
SignatureValidationMessage = "Bitte markieren Sie mindestens ein Unterschriftsfeld im Dokument.";
return;
}
if (_checkedAnnotations.Count < _annotations.Count) {
SignatureValidationMessage = $"Bitte markieren Sie alle {_annotations.Count} Unterschriftsfelder. Noch {_annotations.Count - _checkedAnnotations.Count} offen.";
return;
}
SignatureValidationMessage = null;
var freshReport = BuildFreshBaseReport();
foreach (var ann in _annotations)
AddSignatureAtAnnotation(freshReport, ann, _capturedSignature!.DataUrl, _capturedSignature.FullName, _capturedSignature.Position, _capturedSignature.Place);
Report = freshReport;
SignatureApplied = true;
ViewerKey++;
await InvokeAsync(StateHasChanged);
}
async Task<string?> GetActiveSignatureDataUrlAsync() {
if(ActiveSignatureTab == SignatureTabDraw)
return await JSRuntime.InvokeAsync<string?>("receiverSignature.getDataUrl", DrawCanvasId);
if(ActiveSignatureTab == SignatureTabText) {
await RenderTypedSignatureAsync();
return await JSRuntime.InvokeAsync<string?>("receiverSignature.getTypedDataUrl", TypedCanvasId);
}
return await JSRuntime.InvokeAsync<string?>("receiverSignature.getImageDataUrl", ImageCanvasId);
}
async Task ExportSignedPdfAsync() {
if(!SignatureApplied || Report is null) {
SignatureValidationMessage = "Bitte fuegen Sie die Unterschrift zuerst zum Bericht hinzu.";
return;
}
try {
SignatureValidationMessage = null;
await reportViewer!.ExportToAsync(ExportFormat.Pdf);
} catch(Exception) {
SignatureValidationMessage = "Das signierte PDF konnte nicht exportiert werden. Bitte laden Sie die Seite neu und versuchen Sie es erneut.";
}
}
XtraReport BuildFreshBaseReport() {
if (_basePdfBytes is { Length: > 0 }) {
var report = new XtraReport();
var detail = new DevExpress.XtraReports.UI.DetailBand();
report.Bands.Add(detail);
detail.Controls.Add(new DevExpress.XtraReports.UI.XRPdfContent { Source = _basePdfBytes, GenerateOwnPages = true });
return report;
}
return CreateReportInstance();
}
static void AddAnnotationPlaceholders(XtraReport report, IReadOnlyList<AnnotationDto> annotations) {
var bottomMargin = report.Bands.OfType<BottomMarginBand>().FirstOrDefault();
if (bottomMargin is null) {
bottomMargin = new BottomMarginBand();
report.Bands.Add(bottomMargin);
}
const float sigWidth = 230F;
const float sigHeight = 154F;
const float bottomPad = 6F;
const float defaultTopPad = 8F;
const float maxBandHeight = 210F;
float requiredHeight = defaultTopPad + sigHeight + bottomPad;
bottomMargin.HeightF = Math.Min(maxBandHeight, Math.Max(bottomMargin.HeightF, requiredHeight));
float topPad = Math.Max(0F, bottomMargin.HeightF - bottomPad - sigHeight);
foreach (var ann in annotations) {
float sigX = (float)(ann.X);
var annotId = ann.Id.ToString();
var placeholder = new XRLabel {
Name = $"receiverSignaturePlaceholder_{annotId}",
Text = "\u270e Bitte unterschreiben",
BoundsF = new RectangleF(sigX, topPad, sigWidth, sigHeight),
Borders = BorderSide.All,
BorderColor = System.Drawing.Color.FromArgb(230, 81, 0),
BackColor = System.Drawing.Color.FromArgb(30, 255, 236, 153),
Font = new DXFont("Open Sans", 10F, DXFontStyle.Regular),
ForeColor = System.Drawing.Color.FromArgb(94, 38, 0),
TextAlignment = TextAlignment.MiddleCenter
};
bottomMargin.Controls.Add(placeholder);
}
}
XtraReport CreateReportInstance() {
return ReportStorage.TryGetReport("LargeDatasetReport", out var savedReport)
? savedReport
: PredefinedReports.ReportsFactory.GetReport("LargeDatasetReport");
}
static void AddSignature(XtraReport report, string signatureDataUrl, string signerFullName, string signerPosition, string signaturePlace) {
var imageBytes = Convert.FromBase64String(signatureDataUrl[(signatureDataUrl.IndexOf(',') + 1)..]);
using var imageStream = new MemoryStream(imageBytes);
var imageSource = new ImageSource(DXImage.FromStream(imageStream));
var bottomMargin = report.Bands.OfType<BottomMarginBand>().FirstOrDefault();
if(bottomMargin is null) {
bottomMargin = new BottomMarginBand();
report.Bands.Add(bottomMargin);
}
RemoveExistingSignature(bottomMargin);
// Layout constants
const float sigX = 390F;
const float sigWidth = 230F;
const float sigImgHeight = 70F;
const float infoHeight = 65F; // up to 4 lines at 8pt
const float innerGap = 5F;
const float bottomPad = 6F;
const float defaultTopPad = 8F;
const float maxBandHeight = 210F;
float requiredHeight = defaultTopPad + sigImgHeight + innerGap + infoHeight + bottomPad;
// Grow band if needed, but cap at maxBandHeight to avoid overlapping page content
bottomMargin.HeightF = Math.Min(maxBandHeight, Math.Max(bottomMargin.HeightF, requiredHeight));
// If band is tighter than required, compress top padding so content still fits
float topPad = Math.Max(0F, bottomMargin.HeightF - bottomPad - infoHeight - innerGap - sigImgHeight);
float imageY = topPad;
float labelY = imageY + sigImgHeight + innerGap;
var signatureInformation = string.IsNullOrWhiteSpace(signerPosition)
? $"{signerFullName}\n{signaturePlace}, {DateTime.Now:d}"
: $"{signerFullName}\n{signerPosition}\n{signaturePlace}, {DateTime.Now:d}";
var signature = new XRPictureBox {
Name = "receiverSignatureImage",
ImageSource = imageSource,
BoundsF = new RectangleF(sigX, imageY, sigWidth, sigImgHeight),
Sizing = ImageSizeMode.ZoomImage,
Borders = BorderSide.Bottom,
BorderColor = System.Drawing.Color.FromArgb(73, 80, 87)
};
var signatureLabel = new XRLabel {
Name = "receiverSignatureLabel",
Text = signatureInformation,
Multiline = true,
BoundsF = new RectangleF(sigX, labelY, sigWidth, infoHeight),
Font = new DXFont("Open Sans", 8F, DXFontStyle.Regular),
ForeColor = System.Drawing.Color.FromArgb(73, 80, 87),
TextAlignment = TextAlignment.TopLeft
};
bottomMargin.Controls.AddRange(new XRControl[] { signature, signatureLabel });
}
static void RemoveExistingSignature(BottomMarginBand bottomMargin) {
var controls = bottomMargin.Controls
.Cast<XRControl>()
.Where(control => control.Name is "receiverSignatureLabel" or "receiverSignatureImage")
.ToArray();
foreach(var control in controls)
bottomMargin.Controls.Remove(control);
}
static void AddSignatureAtAnnotation(XtraReport report, AnnotationDto? annotation, string signatureDataUrl, string signerFullName, string signerPosition, string signaturePlace) {
var imageBytes = Convert.FromBase64String(signatureDataUrl[(signatureDataUrl.IndexOf(',') + 1)..]);
using var imageStream = new MemoryStream(imageBytes);
var imageSource = new ImageSource(DXImage.FromStream(imageStream));
var detail = report.Bands.OfType<DevExpress.XtraReports.UI.DetailBand>().FirstOrDefault();
if (detail is null) return;
var annotId = annotation?.Id.ToString() ?? "0";
RemoveExistingSignatureById(detail, annotId);
const float sigWidth = 230F;
const float sigImgHeight = 70F;
const float infoHeight = 48.75F;
const float innerGap = 5F;
float sigX = (float)(annotation?.X ?? 390.0);
float imageY = (float)(annotation?.Y ?? 900.0);
float labelY = imageY + sigImgHeight + innerGap;
int targetPage = annotation?.Page ?? 1;
var signatureInformation = string.IsNullOrWhiteSpace(signerPosition)
? $"{signerFullName}\n{signaturePlace}, {DateTime.Now:d}"
: $"{signerFullName}\n{signerPosition}\n{signaturePlace}, {DateTime.Now:d}";
var signature = new XRPictureBox {
Name = $"receiverSignatureImage_{annotId}",
ImageSource = imageSource,
BoundsF = new RectangleF(sigX, imageY, sigWidth, sigImgHeight),
Sizing = ImageSizeMode.ZoomImage,
Borders = BorderSide.Bottom,
BorderColor = System.Drawing.Color.FromArgb(73, 80, 87),
BackColor = Color.FromArgb(219, 219, 219)
};
var signatureLabel = new XRLabel {
Name = $"receiverSignatureLabel_{annotId}",
Text = signatureInformation,
Multiline = true,
BoundsF = new RectangleF(sigX, labelY - 5, sigWidth, infoHeight),
Font = new DXFont("Open Sans", 8F, DXFontStyle.Regular),
ForeColor = System.Drawing.Color.FromArgb(73, 80, 87),
TextAlignment = TextAlignment.TopCenter,
BackColor = Color.FromArgb(219, 219, 219)
};
// Show each control only on the target page using an independent print counter
int sigPrintCount = 0;
signature.BeforePrint += (_, e) => {
sigPrintCount++;
e.Cancel = sigPrintCount != targetPage;
};
int lblPrintCount = 0;
signatureLabel.BeforePrint += (_, e) => {
lblPrintCount++;
e.Cancel = lblPrintCount != targetPage;
};
detail.Controls.AddRange(new XRControl[] { signature, signatureLabel });
}
static void RemoveExistingSignatureById(DevExpress.XtraReports.UI.DetailBand detail, string annotId) {
var controls = detail.Controls
.Cast<XRControl>()
.Where(c => c.Name == $"receiverSignatureImage_{annotId}" || c.Name == $"receiverSignatureLabel_{annotId}")
.ToArray();
foreach (var c in controls)
detail.Controls.Remove(c);
}
public void Dispose() {
_dotNetRef?.Dispose();
}
}

View File

@@ -1,81 +0,0 @@
@page "/test"
@using System.Drawing
@using DevExpress.Drawing
@using DevExpress.Utils
@using DevExpress.XtraPrinting
@using DevExpress.XtraPrinting.Drawing
@using XtraReport = DevExpress.XtraReports.UI.XtraReport
@using BottomMarginBand = DevExpress.XtraReports.UI.BottomMarginBand
@using XRLabel = DevExpress.XtraReports.UI.XRLabel
@using XRPictureBox = DevExpress.XtraReports.UI.XRPictureBox
@using XRControl = DevExpress.XtraReports.UI.XRControl
@using ImageSizeMode = DevExpress.XtraPrinting.ImageSizeMode
@using EnvelopeGenerator.ReceiverUI.Services
@using DevExpress.Blazor.Reporting
@inject IJSRuntime JSRuntime
@inject InMemoryReportStorageWebExtension ReportStorage
@inject EnvelopeGenerator.ReceiverUI.Services.DocumentService DocumentService
@inject Microsoft.Extensions.Configuration.IConfiguration Configuration
@inject HttpClient Http
@using System;
<link href="_content/DevExpress.Blazor.Themes/blazing-berry.bs5.min.css" rel="stylesheet" />
<link href="_content/DevExpress.Blazor.Reporting.Viewer/css/dx-blazor-reporting-components.bs5.css" rel="stylesheet" />
<div class="receiver-page-layout">
<div class="receiver-viewer-wrapper">
<embed class="w-100 h-50" src="/docs/Document.pdf" type="application/pdf" />
@if (PdfBytes is { Length: > 0 }) {
<DxPdfViewer DocumentContent="PdfBytes" CssClass="w-100 h-50" />
}
else{
<div>Not found</div>
}
</div>
</div>
@code {
const string SignatureTabDraw = "draw";
const string SignatureTabText = "text";
const string SignatureTabImage = "image";
const string DrawCanvasId = "receiver-signature-pad";
const string TypedCanvasId = "receiver-typed-signature-pad";
const string ImageInputId = "receiver-signature-image-input";
const string ImageCanvasId = "receiver-image-signature-pad";
readonly (string Text, string Value)[] TypedSignatureFonts = {
("Brush Script", "'Brush Script MT', cursive"),
("Segoe Script", "'Segoe Script', cursive"),
("Lucida Handwriting", "'Lucida Handwriting', cursive"),
("Comic Sans", "'Comic Sans MS', cursive"),
("Cursive", "cursive")
};
[Parameter] public string? EnvelopeKey { get; set; }
DxReportViewer? reportViewer;
XtraReport? Report;
string PdfViewerUrl = string.Empty;
byte[]? PdfBytes;
byte[]? SignedPdfBytes;
bool SignatureApplied;
bool SignaturePopupVisible;
string? PopupValidationMessage;
string ActiveSignatureTab = SignatureTabDraw;
string TypedSignatureText = string.Empty;
string TypedSignatureFont = "'Brush Script MT', cursive";
string SignerFullName = string.Empty;
string SignerPosition = string.Empty;
string SignaturePlace = string.Empty;
int ViewerKey;
protected override async Task OnInitializedAsync() {
PdfBytes = await Http.GetByteArrayAsync("/docs/Document.pdf");
}
}

View File

@@ -1,45 +0,0 @@
using Microsoft.AspNetCore.Components.WebAssembly.Hosting;
using Microsoft.AspNetCore.Components.Web;
using EnvelopeGenerator.ReceiverUI;
using DevExpress.DataAccess.Web;
using EnvelopeGenerator.ReceiverUI.Services;
using EnvelopeGenerator.ReceiverUI.Options;
using DevExpress.XtraReports.Services;
using DevExpress.Blazor.Reporting;
using DevExpress.XtraReports.Web.Extensions;
var builder = WebAssemblyHostBuilder.CreateDefault(args);
builder.RootComponents.Add<App>("#app");
builder.RootComponents.Add<HeadOutlet>("head::after");
builder.Services.AddScoped(sp => new HttpClient { BaseAddress = new Uri(builder.HostEnvironment.BaseAddress) });
builder.Services.Configure<ApiOptions>(opts =>
builder.Configuration.GetSection(ApiOptions.SectionName).Bind(opts));
builder.Services.Configure<PdfViewerOptions>(opts =>
builder.Configuration.GetSection(PdfViewerOptions.SectionName).Bind(opts));
builder.Services.AddScoped<DocumentService>();
builder.Services.AddScoped<AuthService>();
builder.Services.AddScoped<AnnotationService>();
builder.Services.AddScoped<EnvelopeReceiverService>();
builder.Services.AddScoped<SignatureService>();
builder.Services.AddScoped<SignatureCacheService>();
builder.Services.AddSingleton<AppVersionService>();
builder.Services.AddDevExpressWebAssemblyBlazorReportViewer();
builder.Services.AddDevExpressWebAssemblyBlazorPdfViewer();
builder.Services.AddDevExpressBlazorReportingWebAssembly(configure => {
configure.UseDevelopmentMode();
});
builder.Services.AddScoped<IDataSourceWizardJsonConnectionStorage, CustomDataSourceWizardJsonDataConnectionStorage>();
builder.Services.AddScoped<IJsonDataConnectionProviderFactory, CustomJsonDataConnectionProviderFactory>();
builder.Services.AddScoped<IObjectDataSourceWizardTypeProvider, ObjectDataSourceWizardCustomTypeProvider>();
DevExpress.Utils.DeserializationSettings.RegisterTrustedClass(typeof(EnvelopeGenerator.ReceiverUI.Data.DataItemList));
DevExpress.Utils.DeserializationSettings.RegisterTrustedClass(typeof(EnvelopeGenerator.ReceiverUI.PredefinedReports.Report));
builder.Services.AddSingleton<InMemoryReportStorageWebExtension>();
builder.Services.AddSingleton<ReportStorageWebExtension>(sp => sp.GetRequiredService<InMemoryReportStorageWebExtension>());
builder.Services.AddScoped<IReportProviderAsync, CustomReportProvider>();
ReportStorageWebExtension.RegisterExtensionGlobal(new InMemoryReportStorageWebExtension());
var host = builder.Build();
await FontLoader.LoadFonts(host.Services.GetRequiredService<HttpClient>(), new List<string> { "opensans.ttf" });
await host.RunAsync();

View File

@@ -1,17 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- https://go.microsoft.com/fwlink/?LinkID=208121. -->
<Project>
<PropertyGroup>
<WebPublishMethod>Package</WebPublishMethod>
<LastUsedBuildConfiguration>Release</LastUsedBuildConfiguration>
<LastUsedPlatform>Any CPU</LastUsedPlatform>
<SiteUrlToLaunchAfterPublish />
<LaunchSiteAfterPublish>true</LaunchSiteAfterPublish>
<ExcludeApp_Data>false</ExcludeApp_Data>
<ProjectGuid>fb2d306b-1042-4a70-31ed-f991a1599371</ProjectGuid>
<DesktopBuildPackageLocation>M:\App&amp;Service\0 DD - Smart UP\signFLOW\ReceiverUI\net8\$(Version)\EnvelopeGenerator.ReceiverUI.zip</DesktopBuildPackageLocation>
<PackageAsSingleFile>true</PackageAsSingleFile>
<DeployIisAppPath>EnvelopeGenerator.ReceiverUI</DeployIisAppPath>
<_TargetId>IISWebDeployPackage</_TargetId>
</PropertyGroup>
</Project>

View File

@@ -1,12 +0,0 @@
{
"profiles": {
"EnvelopeGenerator.ReceiverUI": {
"commandName": "Project",
"launchBrowser": true,
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
},
"applicationUrl": "https://localhost:52936;http://localhost:52937"
}
}
}

View File

@@ -1,33 +0,0 @@
using System.Net.Http.Json;
using System.Text.Json;
using EnvelopeGenerator.ReceiverUI.Models;
using EnvelopeGenerator.ReceiverUI.Options;
using Microsoft.Extensions.Options;
namespace EnvelopeGenerator.ReceiverUI.Services;
/// <summary>
/// Retrieves annotation positions from the API.
/// The URL is composed as <c>{BaseUrl}/api/Annotation/{envelopeKey}</c>.
/// During development, <c>BaseUrl</c> is empty so the request resolves to the
/// YARP-proxied route on the same origin, which currently serves
/// <c>fake-data/annotations.json</c>. To switch to real data, update the
/// YARP route in <c>yarp.json</c> — no code change required.
/// </summary>
[Obsolete("Use SignatureService.")]
public class AnnotationService(HttpClient http, IOptions<ApiOptions> apiOptions)
{
private static readonly JsonSerializerOptions _jsonOptions = new(JsonSerializerDefaults.Web);
public async Task<IReadOnlyList<AnnotationDto>> GetAnnotationsAsync(string envelopeKey, CancellationToken cancel = default)
{
var url = $"{apiOptions.Value.BaseUrl}/api/Annotation/{Uri.EscapeDataString(envelopeKey)}";
var response = await http.GetAsync(url, cancel);
if (!response.IsSuccessStatusCode)
return [];
var result = await response.Content.ReadFromJsonAsync<List<AnnotationDto>>(_jsonOptions, cancel);
return result ?? [];
}
}

View File

@@ -1,57 +0,0 @@
using System.Net;
using EnvelopeGenerator.ReceiverUI.Options;
using Microsoft.Extensions.Options;
namespace EnvelopeGenerator.ReceiverUI.Services;
public enum EnvelopeLoginResult { Success, InvalidCode, NotFound, 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;
}
}

View File

@@ -1,40 +0,0 @@
using DevExpress.DataAccess.Json;
using DevExpress.DataAccess.Web;
using DevExpress.DataAccess.Wizard.Services;
namespace EnvelopeGenerator.ReceiverUI.Services
{
public class CustomDataSourceWizardJsonDataConnectionStorage : IDataSourceWizardJsonConnectionStorage
{
public static JsonDataConnection GetDefaultConnection() {
var uriJsonSource = new UriJsonSource() {
Uri = new Uri(@"https://raw.githubusercontent.com/DevExpress-Examples/DataSources/master/JSON/customers.json"),
};
return new JsonDataConnection(uriJsonSource) { StoreConnectionNameOnly = true, Name = "NWindProductsJson" };
}
public static List<JsonDataConnection> GetConnections() {
var connections = new List<JsonDataConnection> {
GetDefaultConnection()
};
return connections;
}
bool IJsonConnectionStorageService.CanSaveConnection => false;
bool IJsonConnectionStorageService.ContainsConnection(string connectionName) {
return GetConnections().Any(x => x.Name == connectionName);
}
IEnumerable<JsonDataConnection> IJsonConnectionStorageService.GetConnections() {
return GetConnections();
}
JsonDataConnection IJsonDataConnectionProviderService.GetJsonDataConnection(string name) {
var connection = GetConnections().FirstOrDefault(x => x.Name == name);
if(connection == null)
throw new InvalidOperationException();
return connection;
}
void IJsonConnectionStorageService.SaveConnection(string connectionName, JsonDataConnection connection, bool saveCredentials) { }
}
}

Some files were not shown because too many files have changed in this diff Show More