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.
This commit is contained in:
2026-09-25 09:17:50 +02:00
parent 09257d7766
commit 435be882c9
6 changed files with 0 additions and 1046 deletions

View File

@@ -50,19 +50,6 @@ This section explains every markdown file in the repo: what it contains, why it
---
### Ticket / Feature Plans (Active Work Items)
#### `EnvelopeGenerator.Server/SF-74-PLAN.md`
**What:** Implementation plan for SF-74 ticket — Theme selection, Dark Mode, Grid Layout Persistence, signFLOW logo, and customer logo area on the sender dashboard. Includes the original ticket requirements, component breakdown, and implementation steps.
**Why it exists:** Captures the agreed scope and implementation decisions for SF-74 so work can be resumed or reviewed without re-reading the ticket system.
**When you need it:**
- Resuming work on theme, dark mode, or grid persistence features
- Checking what was agreed for logo positioning and size
- Verifying which features of SF-74 are complete vs. pending
**Maintenance:** Mark items as complete as they are implemented. Archive when the ticket is closed.
---
### Deployment Docs
#### `EnvelopeGenerator.Server/EnvelopeGenerator.Server/README.md`
@@ -76,33 +63,6 @@ This section explains every markdown file in the repo: what it contains, why it
---
### Fix Reports (Historical — Read-Only)
These files document completed bug fixes. They exist as audit trails and as references if a similar issue resurfaces. Do not modify them.
#### `fix-report-label-read-and-confirmed.md`
**What:** Documents the fix for an incorrect label in the Signature Certificate report (`Signierungszertifikat`). For "Read and Sign" envelope type (`EnvelopeTypeId = 2`), the history event showed "Document signed" instead of "Read and confirmed" / "Gelesen und bestätigt".
**Branch:** `fix/report-label-read-and-confirmed` — **COMPLETED**
**When you need it:** If a similar report label regression appears, or when adding new envelope type-specific label logic to the certificate report.
#### `fix-signature-field-formatting.md`
**What:** Documents a signature stamp layout fix triggered by WISAG customer feedback (email 3/7/2026). The finalized PDF's signature stamp had incorrect spacing: certificate hash too close to name, position text wrapping, no spacing before date. Fix was applied in the GdPicture14 PDF burning pipeline — not in the Blazor UI.
**When you need it:** If signature stamp formatting regresses, or when modifying the PDF stamping/burning pipeline in the API or infrastructure layer.
---
### Service Migration Docs
#### `EnvelopeGenerator.Service/MIGRATION_PLAN.md`
**What:** Migration plan (v3) for porting `EnvelopeGenerator.Service_legacy` (VB.NET Windows Service) to `EnvelopeGenerator.Service` (C# Worker Service, .NET Framework 4.6.2). Documents connection string handling, job scheduling (Quartz → IHostedService), and deep analysis of legacy code.
**Why it exists:** The Windows Service handles background jobs (email dispatch, envelope finalization). This plan ensures the port is complete and no job logic is silently dropped.
**When you need it:**
- Modifying background jobs (email sending, PDF finalization, access code dispatch)
- Debugging service startup or job execution issues
- Checking if a specific legacy VB.NET job has been ported
---
## Active Architecture (Post-Migration)
**Frontend:** Blazor Auto (Server+WASM hybrid)

View File

@@ -1,442 +0,0 @@
# SF-74 — signFLOW: Theme, Dark Mode, Grid Persistence, Logo & Dashboard
**Ticket:** SF-74 | **Bearbeiter:** Hakan Tek | **Status:** In Bearbeitung
**Letzte Aktualisierung:** 2026-09-24
**Erstellt von:** Marlon Schreiber | **Verknüpft mit:** SF-71
---
## Originale Anforderungen
### Marlon Schreiber (Ticket-Text):
1. **signFLOW-Logo** + **Kunden-Logo-Bereich** auf der Startseite
2. **DevExpress Skin-Auswahl** — mindestens 3 Themes, vom User wählbar + gespeichert
3. **Grid-Layout-Persistenz** — Spaltenbreite/Position pro User gespeichert
### Screenshot-Annotationen (`/sender`):
```
[signFLOW Logo] Umschlag-Übersicht [Kunden-Logo] [+Neuer Umschlag][Bearb.][Lösch.][↺][→|]
signFLOW Portal-Übersicht [Zurückrufen/löschen][Dok.anzeigen][Excel]
[Info-Banner: "Die Größe und Position der Spalten muss verändert und für den User gespeichert..."]
ID ↓ Titel Status [Typ] Empfänger [Erstellt] [Geändert am]
```
---
## Projektstruktur — Clean Architecture
```
EnvelopeGenerator.Domain/ → Entities, Domain-Interfaces
EnvelopeGenerator.Application/ → Service-Interfaces, DTOs, Use-Cases
EnvelopeGenerator.Infrastructure/ → Cache, DB, Implementierungen
EnvelopeGenerator.Server/ → Blazor SSR Host, Controller, Middleware
EnvelopeGenerator.Server.Client/ → Blazor WASM Client
```
**Abhängigkeitsregel:** `Domain ← Application ← Infrastructure ← Server / Server.Client`
---
## ✅ Abgeschlossene Phasen (1–5, 6a–6e)
| Phase | Inhalt | Status |
|-------|--------|--------|
| 1 | Server: UserPreferences API (TBDD_CACHE, `IUserPreferencesService`) | ✅ |
| 2 | WASM: `ThemeService`, `UserPreferencesService` (1h In-Memory-Cache) | ✅ |
| 3 | Footer Theme Slider (🌙 blazing-dark ── blazing-berry ── purple ☀️) | ✅ |
| 4 | Grid Layout Persistence (`LayoutAutoSaving`/`LayoutAutoLoading`) | ✅ |
| 5 | Theme-CSS-Overrides für alle Seiten (`[data-sf-theme]`) | ✅ |
| 6a | signFLOW-Logo (CustomImages Options, Controller, Service, Dateien) | ✅ |
| 6b | Kunden-Logo Placeholder (sender-customer-logo-area, CSS) | ✅ |
| 6c | Grid-Spalten (EnvelopeTypeTitle, AddedWhen, ChangedWhen) + Persistence Hint | ✅ |
| 6d | Dokument anzeigen (PDF Preview Popup mit DxPdfViewer) | ✅ |
| 6e | Export (XLSX/CSV/PDF + Selected Rows Only) | ✅ |
**Key decisions:** TBDD_CACHE statt eigener DB-Tabelle · `SenderOrReceiver` Policy · Slider statt Toggle · `signflow.theme` in localStorage · DevExpress 25.2.3 hat keine `.dark.`-Suffix-Dateien
---
## ✅ Phase 6a — signFLOW-Logo
### Ziel
signFLOW-App-Logo im Sender Dashboard (`/sender`) Action Bar links neben dem Titel, sowie auf der Startseite (`/`) im Hero-Header.
### Technischer Ansatz
**Options Pattern** — `appsettings.json` → `CustomImagesOptions` → DI → Controller → WASM Service.
```
appsettings.json
└── CustomImagesOptions (Options Pattern, IOptions<>)
└── GET /api/CustomImages (neuer Controller, auth: SenderOrReceiver)
└── CustomImagesService (WASM, in-memory cache, kein TTL)
└── EnvelopeSenderPage + IndexPage
```
**Warum kein `wwwroot/appsettings.json`?**
Statische Assets sind public (keine Auth). Logo-Pfade sind deployment-spezifisch.
Options Pattern erlaubt Produktionswechsel ohne Code-Änderung.
### Neue/geänderte Dateien
#### `Server/Models/CustomImages.cs` — NEU
```csharp
public sealed class CustomImageEntry
{
public string Src { get; set; } = string.Empty;
/// <summary>CSS height, e.g. "28px" or "2rem". Change per deployment in appsettings.</summary>
public string Height { get; set; } = "28px";
public Dictionary<string, string> Classes { get; set; } = new();
public string GetClass(string key) =>
Classes.TryGetValue(key, out var cls) ? cls ?? string.Empty : string.Empty;
}
public sealed class CustomImagesOptions
{
public const string SectionName = "CustomImages";
public CustomImageEntry App { get; set; } = new();
public CustomImageEntry Company { get; set; } = new();
}
```
#### `appsettings.json` — Änderung (Zeile 145)
```json
"CustomImages": {
"App": { "Src": "/img/DD_signFLOW_LOGO.png", "Height": "28px",
"Classes": { "Main": "signflow-app-logo" } },
"Company": { "Src": "/img/digital_data.svg", "Height": "22px",
"Classes": { "Show": "signflow-company-logo" } }
}
```
> **Deployment:** Nur `Src` + `Height` in `appsettings.json` ändern — keine Code-Anpassung nötig.
#### `Server/Controllers/CustomImagesController.cs` — NEU
```
GET /api/CustomImages
Auth: [Authorize(Policy = AuthPolicy.SenderOrReceiver)]
Returns: CustomImagesOptions
```
#### `Server/Program.cs` — DI
```csharp
builder.Services.Configure<CustomImagesOptions>(
config.GetSection(CustomImagesOptions.SectionName));
```
#### `Server/wwwroot/img/` — Neue Dateien
Kopiert aus `EnvelopeGenerator.Web/wwwroot/img/`:
- `DD_signFLOW_LOGO.png`
- `digital_data.svg`
#### `Client/Services/CustomImagesService.cs` — NEU
```csharp
// GET /api/CustomImages → einmalig laden, in-memory gecacht (Config ändert sich kaum)
public class CustomImagesService(IHttpClientFactory factory)
{
private CustomImagesDto? _cached;
public async Task<CustomImagesDto> GetAsync(CancellationToken ct = default) { ... }
}
```
#### `app.css` — Neue Klassen
```css
.signflow-app-logo { height: var(--sf-app-height, 28px); width: auto; object-fit: contain; }
.signflow-company-logo { height: var(--sf-company-height, 22px); width: auto; object-fit: contain; opacity: .88; }
```
#### `IndexPage.razor` — Änderung
```razor
<!-- Alt: <svg class="home-hero-header__icon"> -->
<!-- Neu: -->
<img src="@_appLogo.Src" style="height: @_appLogo.Height"
class="home-hero-header__icon" alt="signFLOW" />
```
#### `EnvelopeSenderPage.razor` — signFLOW Logo im Action Bar
```razor
<div class="sender-branding">
<img src="@_appLogo.Src" style="height: @_appLogo.Height"
class="@_appLogo.GetClass("Main")" alt="signFLOW" />
<div class="sender-title-block">
<div class="sender-title">Umschlag-Übersicht</div>
<div class="sender-subtitle">signFLOW Portal - Übersicht</div>
</div>
</div>
```
---
## ✅ Phase 6b — Kunden-Logo Placeholder
### Ziel
Mittlerer Bereich im Action Bar zeigt das Kunden-Logo (aus Config) oder einen Platzhalter.
### Implementierung
**Config-Quelle:** `CustomImagesOptions.Company` (aus Phase 6a bereits geladen)
```razor
<!-- Mitte des Action Bars -->
<div class="sender-customer-logo-area">
@if (!string.IsNullOrEmpty(_companyLogo?.Src))
{
<img src="@_companyLogo.Src" style="height: @_companyLogo.Height"
class="@_companyLogo.GetClass("Show")" alt="Kunden-Logo" />
}
else
{
<div class="sf-customer-logo-placeholder">Kunden-Logo</div>
}
</div>
```
#### `app.css` — Neue Klasse
```css
.sf-customer-logo-placeholder {
height: 24px; min-width: 90px;
border: 1px dashed rgba(126, 34, 206, 0.3); border-radius: 4px;
display: flex; align-items: center; padding: 0 10px;
font-size: 0.62rem; color: rgba(126, 34, 206, 0.4); white-space: nowrap;
}
```
#### `sender-page.css` — Neues Layout (3-Bereich-Action-Bar)
```css
.sender-branding { display: flex; align-items: center; gap: .75rem; flex-shrink: 0; }
.sender-subtitle { font-size: .72rem; color: #6b7280; font-weight: 400; }
.sender-customer-logo-area { flex: 1; display: flex; justify-content: center; }
```
---
## ✅ Phase 6c — Grid: Fehlende Spalten + Persistence Hint
### Fehlende Spalten (aus Screenshot-Annotationen)
**Verfügbarkeit in `EnvelopeDto`:**
- `EnvelopeTypeTitle` (string?) → `spalte für "Typ"` ✅
- `AddedWhen` (DateTime) → `spalte für "erstellt"` ✅
- `ChangedWhen` (DateTime?) → `Spalte "zuletzt geändert am"` ✅
> ⚠️ **Vor Implementierung prüfen:** `EnvelopeTypeTitle` wird aus dem Navigation Property `EnvelopeType` gemappt. Mapping-Konfiguration in `Application/...MappingProfile.cs` verifizieren.
#### `EnvelopeSenderPage.razor` — Neue Spalten (beide Grids)
```razor
<DxGridDataColumn FieldName="EnvelopeTypeTitle" Caption="Typ"
Width="120px" AllowSort="true" />
<DxGridDataColumn FieldName="AddedWhen" Caption="Erstellt"
DisplayFormat="{0:dd.MM.yyyy}" Width="110px" AllowSort="true" />
<DxGridDataColumn FieldName="ChangedWhen" Caption="Geändert am"
DisplayFormat="{0:dd.MM.yyyy}" Width="110px" AllowSort="true" />
```
### Grid Persistence Hint
Info-Banner über dem Grid:
```razor
<div class="sf-grid-persistence-hint">
<svg><!-- info icon --></svg>
Die Größe und Position der Spalten wird automatisch für Sie gespeichert.
</div>
```
```css
.sf-grid-persistence-hint {
display: flex; align-items: center; gap: 6px;
padding: 5px 16px; font-size: 0.72rem;
color: rgba(126, 34, 206, 0.7);
background: rgba(126, 34, 206, 0.04);
border-bottom: 1px solid rgba(126, 34, 206, 0.08);
}
```
---
## ✅ Phase 6d — Dokument anzeigen (PDF Preview)
### Anforderung
Ausgewähltes Envelope-Dokument im Popup anzeigen. Envelope ist nach dem Senden **read-only** (Mails wurden gesendet — nachträgliche Änderungen würden Konsistenz brechen).
### Technischer Ansatz
`DxPdfViewer` erfordert **zwingend `@rendermode InteractiveServer`**.
`EnvelopeSenderPage` ist WASM (`@rendermode InteractiveWebAssembly`).
**Lösung:** `DxPopup` im WASM-Parent mit einem Server-Component darin:
```
EnvelopeSenderPage.razor (@rendermode WASM)
└── <DxPopup>
└── <EnvelopePdfPreviewComponent @rendermode InteractiveServer>
└── <DxPdfViewer> (SSR-only)
```
#### `Server/Components/Shared/EnvelopePdfPreviewComponent.razor` — NEU
```razor
@rendermode InteractiveServer
@inject IDocumentService DocumentService <!-- oder HTTP client -->
<DxPdfViewer DocumentContent="@_pdfBytes" />
@code {
[Parameter] public int EnvelopeId { get; set; }
byte[]? _pdfBytes;
protected override async Task OnParametersSetAsync()
{
if (EnvelopeId > 0)
_pdfBytes = await DocumentService.GetPdfBytesAsync(EnvelopeId);
}
}
```
Button: nur aktiv wenn Envelope ausgewählt UND Dokument vorhanden (`envelope.Documents?.Any() == true`).
---
## ✅ Phase 6e — Export (XLSX/CSV/PDF)
### Technischer Ansatz
`DxGrid`-Export direkt im Razor-Code, keine Backend-Implementierung nötig.
Download geht direkt an den Browser.
```csharp
async Task ExportSelectedFormatAsync()
{
var grid = _activeTab == "active" ? _gridActive : _gridCompleted;
if (grid is null) return;
var baseName = $"Umschlaege_{DateTime.Now:yyyy-MM-dd}";
switch (_selectedExportFormat)
{
case "csv":
await grid.ExportToCsvAsync(baseName, new GridCsvExportOptions {
ExportSelectedRowsOnly = _exportSelectedRowsOnly
});
break;
case "pdf":
await grid.ExportToPdfAsync(baseName, new GridPdfExportOptions {
ExportSelectedRowsOnly = _exportSelectedRowsOnly,
SelectedRowsExportMode = GridSelectedRowsExportMode.KeepGrouping
});
break;
default:
await grid.ExportToXlsxAsync(baseName, new GridXlExportOptions {
ExportSelectedRowsOnly = _exportSelectedRowsOnly,
SelectedRowsExportMode = GridSelectedRowsExportMode.KeepGrouping
});
break;
}
}
```
> **Hinweis:** `CellDisplayTemplate`-Inhalte werden nicht exportiert (DevExpress-Limitierung).
> Die Spalten `Status`, `EnvelopeReceivers` nutzen Templates → Wert im Export ist leer.
> **Future Task:** Für vollständigen Export mit formatierten Zellen `CustomizeCell`-Event nutzen.
UI in Toolbar (kompakt):
```razor
<select @bind="_selectedExportFormat">
<option value="xlsx">XLSX</option>
<option value="csv">CSV</option>
<option value="pdf">PDF</option>
</select>
<input type="checkbox" @bind="_exportSelectedRowsOnly" /> Nur ausgewählte
<button @onclick="ExportSelectedFormatAsync">Exportieren</button>
```
---
## 🔲 Phase 6f — Action Bar Redesign
### Ziel-Layout (aus Screenshot)
```
┌──────────────────────────────────────────────────────────────────────────────┐
│ [Logo] Umschlag-Übersicht │ [Kunden-Logo] │ [+Neu] [Bearb.] [Lösch.] │
│ signFLOW Portal-Übers│ │ [↺] [→|] │
│ │ │ [Zurückrufen?] [Dok.] [XLS] │
└──────────────────────────────────────────────────────────────────────────────┘
```
### Neue CSS-Klassen in `sender-page.css`
```css
.sender-toolbar-right { display: flex; flex-direction: column; gap: .35rem; align-items: flex-end; }
.sender-toolbar-row { display: flex; align-items: center; gap: .5rem; }
.sender-toolbar-row--primary { /* Hauptbuttons */ }
.sender-toolbar-row--secondary { /* Dokument anzeigen, Export */ }
```
### `Zurückrufen/löschen` — Offene Frage ❓
> **Ticket-Kommentar-Entwurf (Deutsch, für Marlon Schreiber):**
>
> Hallo @Marlon Schreiber,
>
> im Screenshot sehe ich einen Button „Zurückrufen/löschen". Könntest du den
> Unterschied zum bereits vorhandenen „Löschen"-Button erläutern?
>
> Meine Vermutung: „Zurückrufen" bedeutet das Widerrufen eines **bereits gesendeten**
> Umschlags (Status → `EnvelopeWithdrawn`), während „Löschen" einen noch nicht
> gesendeten Umschlag entfernt. Ist das korrekt? Und falls ja:
> - Sollen Empfänger bei einem Widerruf automatisch benachrichtigt werden?
> - Soll der Status auf `Zurückgerufen` gesetzt werden und der Umschlag
> weiterhin in der Liste erscheinen (aber inaktiv)?
>
> Bitte kurz antworten, damit ich dies korrekt implementieren kann.
---
## Implementierungsreihenfolge Phase 6
```
6a (Logo) → 6b (Kunden-Logo) → 6c (Grid-Spalten) → 6d (PDF Preview) → 6e (Excel) → 6f (Action Bar)
```
Jede Teilphase ist unabhängig deploybar. `6f` fasst alle Layout-Änderungen zusammen
und sollte zuletzt implementiert werden, wenn alle Komponenten fertig sind.
---
## Bekannte Einschränkungen & Offene Punkte
| # | Thema | Status |
|---|-------|--------|
| 1 | `DxPdfViewer` → InteractiveServer zwingend | Architektonisch gelöst (6d) |
| 2 | Export: Template-Spalten leer im Export | Akzeptiert, Future Task (CustomizeCell) |
| 3 | `Zurückrufen/löschen` — Bedeutung unklar | Kommentar-Frage vorbereitet (6f) |
| 4 | `EnvelopeTypeTitle` Mapping prüfen | Vor 6c verifizieren |
| 5 | Logo-Pfade produktionsspezifisch | Via `appsettings.json` konfigurierbar |
| 6 | Server-seitige Pages — inline Farben bleiben | Out-of-scope |
---
## Nicht in diesem Ticket
- `Einstellungen`-Seite (separates Ticket)
- Mobile Responsive (separates Ticket)
- Excel CustomizeCell vollständige Implementierung (Future Task)
- Widerruf-Backend (wartet auf Klärung)
---
*Erstellt: 2026-09-01 | Aktualisiert: 2026-09-03 | Ticket: SF-74 | Projekt: signFLOW*

View File

@@ -1,315 +0,0 @@
# Migration Plan: EnvelopeGenerator.Service_legacy ? EnvelopeGenerator.Service
**Revision v3** — Full deep analysis complete
## Overview
Migrate the legacy VB.NET Windows Service (`EnvelopeGenerator.Service_legacy`) to a modern
C# Worker Service (`EnvelopeGenerator.Service`) targeting **.NET Framework 4.6.2**, using
`Microsoft.Extensions.Hosting`, `IConfiguration`, `ILogger<T>` and **NLog**.
---
## Deep Analysis Findings (vs. v1/v2)
### Finding 1 — Connection String: Plain Text, No Encryption Required
`FinalizeDocumentJob` and `APIEnvelopeJob` call `MSSQLServer.DecryptConnectionString()` on the
`JobDataMap[Value.DATABASE]` value. However, `MSSQLServer.DecryptConnectionString()` is a pass-through
for plain-text strings — no wrapping/encryption step is needed.
**Resolution**: The connection string is read from `IConfiguration` via the standard
`ConnectionStrings:Default` key (same key as `appsettings.Database.json` in `EnvelopeGenerator.Web`).
It is passed **as-is** to both the Worker-level `MSSQLServer` and the `JobDataMap`. No
`EncryptConnectionString()` call is made anywhere in the new service.
### Finding 2 — Dual TempFiles Architecture
There are **two independent** `TempFiles` classes:
- `EnvelopeGenerator.CommonServices.TempFiles` — used **inside each job** via `New TempFiles(LogConfig)`.
This is called on every job execution and manages the job's own temp lifecycle. It expects `LogConfig`.
**We do not touch this.**
- `EnvelopeGenerator.Service_legacy.TempFiles` — used at the **service/host level** (startup cleanup,
shutdown cleanup). This is the one we rewrite in C# in the Service project using `ILogger<TempFiles>`.
### Finding 3 — LogConfig is a Hard Dependency of All Jobs
`FinalizeDocumentJob.Execute()` and `APIEnvelopeJob.Execute()` both do:
```vb
LogConfig = pContext.MergedJobDataMap.Item(Value.LOGCONFIG)
Logger = LogConfig.GetLogger()
myTempFiles = New TempFiles(LogConfig) ' ? CommonServices.TempFiles
```
`LogConfig` is **not optional**. The schedulers must construct a valid `LogConfig` and place it in the
`JobDataMap`. It must carry the correct log path, debug flag, and application identity.
**Resolution**: A `LogConfigFactory` static helper class will be created in the Service project to
build a `LogConfig` from `ServiceConfig` values:
- `LogPath` = `Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "Log")` (mirrors legacy `My.Application.Info.DirectoryPath + "\Log"`)
- `Debug` = `ServiceConfig.Debug`
- Application name = `"EnvelopeGenerator.Service"`
### Finding 4 — ProjectInstaller is Obsolete
`ProjectInstaller.vb` was the old `installutil.exe` mechanism for Windows Service registration.
With `AddWindowsService()` + `sc create` / PowerShell `New-Service`, this is **completely replaced**.
No equivalent is needed in the new project.
### Finding 5 — GDPicture License SQL
The legacy service queries: `SELECT LICENSE FROM TBDD_3RD_PARTY_MODULES WHERE NAME = 'GDPICTURE' and ACTIVE = 1`
This must be replicated exactly in the Worker startup sequence. If the result is null/empty, startup fails
with a descriptive exception (matching legacy behavior).
### Finding 6 — Quartz Version Mismatch
`CommonServices` uses `Quartz 3.8.0`. `Service_legacy` uses `Quartz 3.15.0`.
The new `EnvelopeGenerator.Service` must use **Quartz 3.8.0** to match `CommonServices` (same AppDomain,
same version must be loaded). The legacy version bump in `Service_legacy` was inconsistent.
---
## 1. Project File (`EnvelopeGenerator.Service.csproj`)
- Add **NuGet packages**:
- `NLog` (5.x, matching CommonServices' `NLog.5.0.5`)
- `NLog.Extensions.Logging` (latest net462-compatible)
- `Quartz` (**3.8.0** — must match CommonServices)
- `Microsoft.Extensions.Hosting.WindowsServices` (for `AddWindowsService()`)
- Add **project reference** to `EnvelopeGenerator.CommonServices` (vbproj)
- Add **project reference** to `EnvelopeGenerator.Domain` (csproj)
---
## 2. Configuration (`ServiceConfig.cs`)
```csharp
public class ServiceConfig
{
public string ConnectionString { get; set; } = string.Empty;
public bool Debug { get; set; } = false;
public int IntervalInMin { get; set; } = 1;
public PDFBurnerParams PDFBurnerParams { get; set; } = new PDFBurnerParams();
}
```
Connection string is read via the standard `ConnectionStrings:Default` key — consistent with
`appsettings.Database.json` used by `EnvelopeGenerator.Web`:
`appsettings.json`:
```json
{
"ConnectionStrings": {
"Default": ""
},
"ServiceConfig": {
"Debug": false,
"IntervalInMin": 1,
"PDFBurnerParams": {
"IgnoredLabels": [ "Date", "Datum", "ZIP", "PLZ", "Place", "Ort", "Position", "Stellung" ],
"TopMargin": 0.1,
"YOffset": -0.3,
"FontName": "Arial",
"FontSize": 8,
"FontStyle": 2,
"TruncationSuffix": "..",
"TextMaxWidths": {
"position": 1.41,
"city": 1.41,
"date": 2.0
}
}
}
}
```
> `FontStyle` integer: Regular=0, Bold=1, **Italic=2**, Underline=4, Strikeout=8.
> All values mirror `PDFBurnerParams.vb` defaults exactly.
---
## 3. NLog Setup
**`nlog.config`**: File target ? `${basedir}/Log/service-${shortdate}.log`, async wrapper,
layout with timestamp + level + logger + message + exception.
**`Program.cs`**: Wire via `NLog.Extensions.Logging`:
```csharp
builder.Logging.ClearProviders();
builder.Logging.AddNLog("nlog.config");
```
---
## 4. LogConfigFactory (`LogConfigFactory.cs`)
New static helper — bridges `ServiceConfig` ? `LogConfig` (required by CommonServices jobs):
```csharp
internal static class LogConfigFactory
{
public static LogConfig Create(ServiceConfig config)
{
var logPath = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "Log");
var logConfig = new LogConfig(
LogConfig.PathType.CustomPath, logPath, null,
"Digital Data", "EnvelopeGenerator.Service");
logConfig.Debug = config.Debug;
return logConfig;
}
}
```
This `LogConfig` instance is what gets placed into every `JobDataMap[Value.LOGCONFIG]`.
---
## 5. Quartz LogProvider (`QuartzLogProvider.cs`)
C# rewrite of `LogProvider.vb` — bridges Quartz `ILogProvider` ? `ILogger<T>`:
```csharp
internal class QuartzLogProvider : ILogProvider
{
private readonly ILogger _logger;
public QuartzLogProvider(ILogger logger) => _logger = logger;
public Logger GetLogger(string name) => (level, func, ex, parameters) => { ... };
// OpenNestedContext / OpenMappedContext ? NotImplementedException (same as legacy)
}
```
---
## 6. TempFiles (`TempFiles.cs`) — Service-level only
Service-level rewrite of `Service_legacy/TempFiles.vb` using `ILogger<TempFiles>`:
- `Create()` ? create `%TEMP%\EnvelopeGenerator`, or clean existing files
- `CleanUp()` ? delete the directory on service stop
> **Does NOT replace** `CommonServices.TempFiles` — that one continues to be used
> inside each job via `LogConfig`.
---
## 7. Schedulers
### `Schedulers/SchedulerFinishEnvelope.cs`
```csharp
public class SchedulerFinishEnvelope
{
// Constructor: ILogger<SchedulerFinishEnvelope>, IOptions<ServiceConfig>
// Start(licenseKey, logConfig):
// - Build JobDataMap:
// [GDPICTURE] = licenseKey
// [LOGCONFIG] = logConfig ? LogConfigFactory output
// [DATABASE] = MSSQLServer.EncryptConnectionString(connectionString)
// [PDF_BURNER_PARAMS] = config.PDFBurnerParams
// - Schedule FinalizeDocumentJob with Quartz (interval from config)
// - Register QuartzLogProvider
// Stop(): Scheduler.Shutdown()
}
```
### `Schedulers/SchedulerEnvelopeTaskApi.cs`
```csharp
public class SchedulerEnvelopeTaskApi
{
// Constructor: ILogger<SchedulerEnvelopeTaskApi>, IOptions<ServiceConfig>
// Start(logConfig):
// - Build JobDataMap:
// [LOGCONFIG] = logConfig
// [DATABASE] = MSSQLServer.EncryptConnectionString(connectionString)
// - Schedule APIEnvelopeJob with Quartz (interval from config)
// - Register QuartzLogProvider
// Stop(): Scheduler.Shutdown()
}
```
---
## 8. Worker (`Worker.cs`)
Full lifecycle replacing the placeholder:
```
ExecuteAsync:
1. Read ServiceConfig from IOptions<ServiceConfig>
2. Validate ConnectionString ? throw if empty
3. Build LogConfig via LogConfigFactory
4. Connect to DB (plain connection string — Worker-level only)
5. Query: SELECT LICENSE FROM TBDD_3RD_PARTY_MODULES WHERE NAME='GDPICTURE' AND ACTIVE=1
? throw if null/empty (matches legacy behavior)
6. TempFiles.Create()
7. await SchedulerFinishEnvelope.Start(licenseKey, logConfig)
8. await Task.Delay(2500, stoppingToken) ? preserved from legacy
9. await SchedulerEnvelopeTaskApi.Start(logConfig)
10. await Task.Delay(Timeout.Infinite, stoppingToken) ? wait for cancellation
StopAsync override:
11. await SchedulerFinishEnvelope.Stop()
12. await SchedulerEnvelopeTaskApi.Stop()
13. TempFiles.CleanUp()
```
---
## 9. Program.cs
```csharp
var builder = Host.CreateApplicationBuilder(args);
builder.Logging.ClearProviders();
builder.Logging.AddNLog("nlog.config");
builder.Services.Configure<ServiceConfig>(
builder.Configuration.GetSection("ServiceConfig"));
builder.Services.AddSingleton<TempFiles>();
builder.Services.AddSingleton<SchedulerFinishEnvelope>();
builder.Services.AddSingleton<SchedulerEnvelopeTaskApi>();
builder.Services.AddHostedService<Worker>();
builder.Services.AddWindowsService(o => o.ServiceName = "EnvelopeGenerator.Service");
var host = builder.Build();
host.Run();
```
---
## 10. Files to Create / Modify
| File | Action | Notes |
|------|--------|-------|
| `EnvelopeGenerator.Service.csproj` | **Modify** | Add packages, project refs |
| `Program.cs` | **Modify** | Full DI/NLog/WindowsService setup |
| `Worker.cs` | **Modify** | Full service lifecycle |
| `ServiceConfig.cs` | **Create** | |
| `LogConfigFactory.cs` | **Create** | Critical bridge: ServiceConfig ? LogConfig |
| `TempFiles.cs` | **Create** | Service-level only; CommonServices.TempFiles untouched |
| `QuartzLogProvider.cs` | **Create** | |
| `Schedulers/SchedulerFinishEnvelope.cs` | **Create** | |
| `Schedulers/SchedulerEnvelopeTaskApi.cs` | **Create** | |
| `appsettings.json` | **Create** | All PDFBurnerParams explicit |
| `nlog.config` | **Create** | |
**Not created** (obsolete in new architecture):
- ~~`ProjectInstaller`~~ ? replaced by `sc create` / `New-Service`
- ~~`Service.Designer.vb`~~ ? BackgroundService handles this
- ~~`Service.resx`~~ ? not applicable
---
## 11. Key Design Decisions
| Decision | Rationale |
|----------|-----------|
| **Quartz 3.8.0** (not 3.15.0) | Must match CommonServices; same AppDomain, one version |
| **Connection string plain-text, no encryption** | `MSSQLServer.DecryptConnectionString()` is a pass-through for plain strings; `ConnectionStrings:Default` key used (same convention as Web project) |
| **Connection string in `ConnectionStrings:Default`** | Consistent with rest of the solution (`appsettings.Database.json`); `ServiceConfig` holds only service-specific settings |
| **LogConfigFactory** | Jobs have hard dependency on `LogConfig` in JobDataMap; this is the clean bridge without leaking LogConfig into the DI container |
| **2500ms delay preserved** | Legacy behavior; ensures Scheduler1 is fully initialized before Scheduler2 starts |
| **`AddWindowsService()`** | Native SCM integration; replaces ProjectInstaller/installutil entirely |
| **NLog log path = BaseDirectory/Log** | Mirrors `My.Application.Info.DirectoryPath + "\Log"` from legacy |
| **CommonServices.TempFiles untouched** | Jobs create their own TempFiles internally via LogConfig; service-level TempFiles is separate concern |
---
## Approval Required
> Please review this plan and confirm before implementation begins.

View File

@@ -20,8 +20,6 @@ EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{134D4164-B291-4E19-99B9-E4FA3AFAB62C}"
ProjectSection(SolutionItems) = preProject
COPILOT_CONTEXT.md = COPILOT_CONTEXT.md
fix-report-label-read-and-confirmed.md = fix-report-label-read-and-confirmed.md
fix-signature-field-formatting.md = fix-signature-field-formatting.md
FORM_APPLICATION_CONTEXT.md = FORM_APPLICATION_CONTEXT.md
RECEIVER_PDF_VIEWER_CONTEXT.md = RECEIVER_PDF_VIEWER_CONTEXT.md
EndProjectSection
@@ -45,7 +43,6 @@ EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "EnvelopeGenerator.Server", "EnvelopeGenerator.Server", "{BF1700D5-592E-4FFA-84E8-5480E289A1F0}"
ProjectSection(SolutionItems) = preProject
EnvelopeGenerator.Server\publish.bat = EnvelopeGenerator.Server\publish.bat
EnvelopeGenerator.Server\SF-74-PLAN.md = EnvelopeGenerator.Server\SF-74-PLAN.md
EndProjectSection
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "EnvelopeGenerator.Server", "EnvelopeGenerator.Server\EnvelopeGenerator.Server\EnvelopeGenerator.Server.csproj", "{4E6C54DA-576D-0955-2564-9EC890BB8279}"

View File

@@ -1,135 +0,0 @@
# Fix: Report Label "Read and confirmed" for Read and Sign Envelopes
## Status
✅ **COMPLETED**
## Git Branch
```
fix/report-label-read-and-confirmed
```
---
## Problem
In the **Signature Certificate report** (`Signierungszertifikat`), the history event
list always showed **"Document signed"** for status `DocumentSigned` (code `2005`) —
even when the envelope type was **"Read and Sign"** (`EnvelopeTypeId = 2`).
For "Read and Sign" envelopes the correct label must be **"Read and confirmed"**
(DE: **"Gelesen und bestätigt"**).
### Screenshot reference
The report shows a table with columns *Ereignis | Benutzer | Zeitstempel*.
The row with status 2005 displayed "Document signed" where "Read and confirmed"
was expected.
---
## Root Cause
`ReportItem.ItemStatusTranslated` in
`EnvelopeGenerator.CommonServices\Jobs\FinalizeDocument\ReportItem.vb`
resolved the display label by calling:
```vb
My.Resources.Model.ResourceManager.GetString(ItemStatus.ToString())
```
There was **no branching logic** to handle the Read and Sign envelope type.
The same `DocumentSigned` key was always used regardless of envelope type.
---
## Solution
### Convention followed
The project already uses a `RaC` (**R**ead **a**nd **C**onfirm) suffix for
Read-and-Sign label variants across all resource files:
| Existing RaC keys |
|---|
| `DocumentRejectedRaC` |
| `EnvelopeReportCreatedRaC` |
| `MessageConfirmationSentRaC` |
### New resource key added: `DocumentSignedRaC`
| Resource file | Value |
|---|---|
| `Model.resx` (DE, default) | `Gelesen und bestätigt` |
| `Model.en.resx` (EN) | `Read and confirmed` |
### Logic change in `ReportItem.vb`
```vb
Public ReadOnly Property ItemStatusTranslated As String
Get
Dim oStatusKey = ItemStatus.ToString()
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 Property
```
`Envelope.IsReadAndConfirm()` is defined in
`EnvelopeGenerator.Domain\Interfaces\IEnvelope.cs` and returns `true`
when `EnvelopeTypeId == 2`.
---
## Files Changed
| # | File | Change | Status |
|---|---|---|---|
| 1 | `EnvelopeGenerator.CommonServices\Jobs\FinalizeDocument\ReportItem.vb` | Added `RaC` branching logic to `ItemStatusTranslated` | ? Done |
| 2 | `EnvelopeGenerator.Domain\Resources\Model.resx` | Added `DocumentSignedRaC` = `Gelesen und bestätigt` | ✅ Done |
| 3 | `EnvelopeGenerator.CommonServices\Strings\Model.resx` | Added `DocumentSignedRaC` = `Gelesen und bestätigt` | ✅ Done |
| 4 | `EnvelopeGenerator.CommonServices\Strings\Model.en.resx` | Added `DocumentSignedRaC` = `Read and confirmed` | ✅ Done |
| 5 | `EnvelopeGenerator.Domain\Resources\Model.Designer.cs` | Added `DocumentSignedRaC` strongly-typed property | ✅ Done |
---
## How to resume (for next agent)
All pending items are **resource file additions only**.
Each file needs one new `<data>` block inserted **directly after** the existing
`DocumentSigned` entry:
```xml
<data name="DocumentSignedRaC" xml:space="preserve">
<value>Gelesen und bestätigt</value> <!-- DE files -->
<!-- OR -->
<value>Read and confirmed</value> <!-- EN file -->
</data>
```
`Model.Designer.cs` also needs a new strongly-typed property added after
`DocumentSigned`:
```csharp
/// <summary>
/// Looks up a localized string similar to Gelesen und bestätigt.
/// </summary>
public static string DocumentSignedRaC {
get {
return ResourceManager.GetString("DocumentSignedRaC", resourceCulture);
}
}
```
---
## Testing
After changes, generate a Signature Certificate report for a **Read and Sign**
envelope and verify:
- History row with status 2005 shows **"Read and confirmed"** (EN)
- History row with status 2005 shows **"Gelesen und bestätigt"** (DE)
- Regular (non-RaC) envelopes still show **"Document signed"** / **"Dokument unterzeichnet"**

View File

@@ -1,111 +0,0 @@
# Signature Field Formatting — WISAG Feedback
## Context
Customer feedback received from the new WISAG signFLOW project lead (email dated 3/7/2026).
The signature stamp rendered inside the finalized PDF has layout issues that need to be addressed.
### Current State (screenshot from customer email)
```
Signiert von ___________
[ signature image ]
Armin Schneider
C6B85E96A520C60... ← certificate hash, too close to name
Bereichsleiter Kern- ← position text wraps, no spacing to date
Anwendungen
Frankfurt am Main
23.06.2026, 17:13:42 GMT+2
```
### Issues Reported
1. **Spacing between name and certificate number** is too small — needs more vertical gap.
2. **Spacing between position/title and date/time** is too small — should be dynamic (expand with content).
3. *(Secondary, out of scope here)* Multiple signature fields side-by-side scale individually, causing uneven heights.
---
## Agreed Solution
> **"Height stays fixed. Text stretches horizontally. Beyond a certain character limit, truncate with '...'"**
> *(Original: "Höhe bleibt fix. Text dehnt sich horizontal aus. Ab einem bestimmten Limit wird mit '...' abgekürzt.")*
### Rationale
- The signature field dimensions are **fixed** (`1.77" × 1.96"`). Allowing text to wrap causes position and date rows to collide vertically.
- Truncating long strings with ellipsis (`...`) keeps the layout deterministic and prevents overflow.
- Per-field max-length limits allow tuning each text row independently (name, certificate, position, city, date).
---
## Implementation Plan
### 1. `PDFBurnerParams.vb` — Add per-field max-length dictionary
Add a new property `TextMaxLengths` that maps each field name (matching `EGName` keys) to a maximum character count.
```vbnet
' EnvelopeGenerator.CommonServices/Jobs/FinalizeDocument/PDFBurnerParams.vb
Public Property TextMaxLengths As New Dictionary(Of String, Integer) From {
{"name", 28},
{"position", 32},
{"city", 24},
{"date", 28}
}
```
Default values are starting points and should be tuned against real customer data.
### 2. `PDFBurner.vb` — Add truncation helper and apply in `AddFormFieldValue`
#### 2a. Add a private helper function
```vbnet
Private Function TruncateText(value As String, fieldName As String) As String
If String.IsNullOrEmpty(value) Then Return value
Dim maxLen As Integer = 0
If _pdfBurnerParams.TextMaxLengths IsNot Nothing AndAlso
_pdfBurnerParams.TextMaxLengths.TryGetValue(fieldName, maxLen) AndAlso
maxLen > 0 AndAlso value.Length > maxLen Then
Return value.Substring(0, maxLen - 3) & "..."
End If
Return value
End Function
```
#### 2b. Apply truncation in the `egName`-aware overload
In `AddFormFieldValue(pAnnotation As Annotation, formFieldValue As FormFieldValue)`:
```vbnet
' Before passing value to AddTextAnnot, truncate:
Dim displayValue As String = TruncateText(formFieldValue.value, pAnnotation.egName)
Dim ant = Manager.AddTextAnnot(oX, oY, oWidth, oHeight, displayValue)
```
#### 2c. Apply truncation in the element-based overload (optional)
In `AddFormFieldValue(x, y, width, height, page, value)` — if a field name is available in calling context, pass it through; otherwise this path does not use `egName` and truncation can be skipped or applied with a generic fallback key.
---
## Affected Files
| File | Change |
|------|--------|
| `EnvelopeGenerator.CommonServices/Jobs/FinalizeDocument/PDFBurnerParams.vb` | Add `TextMaxLengths As Dictionary(Of String, Integer)` |
| `EnvelopeGenerator.CommonServices/Jobs/FinalizeDocument/PDFBurner.vb` | Add `TruncateText()` helper; apply in `AddFormFieldValue` overloads |
---
## Notes
- Field names (`"name"`, `"position"`, `"city"`, `"date"`) must match the keys used in `EGName.Index` dictionary (`PDFBurner.vb:443–450`).
- The certificate hash field (e.g. `C6B85E96A520C60...`) is already truncated upstream — verify its `egName` mapping if it needs its own limit.
- The `YOffset` / `TopMargin` values in `PDFBurnerParams` may also need adjustment to increase vertical spacing between rows. That is a separate (simpler) change.
- This issue is **not** related to the Blazor WebUI rendering — it is purely in the GdPicture14 PDF burning pipeline.