diff --git a/AGENTS.md b/AGENTS.md index 396e2211..1e8e86ab 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -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) diff --git a/EnvelopeGenerator.Server/SF-74-PLAN.md b/EnvelopeGenerator.Server/SF-74-PLAN.md deleted file mode 100644 index d427776d..00000000 --- a/EnvelopeGenerator.Server/SF-74-PLAN.md +++ /dev/null @@ -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; - /// CSS height, e.g. "28px" or "2rem". Change per deployment in appsettings. - public string Height { get; set; } = "28px"; - public Dictionary 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( - 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 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 - - -signFLOW -``` - -#### `EnvelopeSenderPage.razor` — signFLOW Logo im Action Bar - -```razor -
- signFLOW -
-
Umschlag-Übersicht
-
signFLOW Portal - Übersicht
-
-
-``` - ---- - -## ✅ 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 - -
- @if (!string.IsNullOrEmpty(_companyLogo?.Src)) - { - Kunden-Logo - } - else - { -
Kunden-Logo
- } -
-``` - -#### `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 - - - -``` - -### Grid Persistence Hint - -Info-Banner über dem Grid: - -```razor -
- - Die Größe und Position der Spalten wird automatisch für Sie gespeichert. -
-``` - -```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) - └── - └── - └── (SSR-only) -``` - -#### `Server/Components/Shared/EnvelopePdfPreviewComponent.razor` — NEU - -```razor -@rendermode InteractiveServer -@inject IDocumentService DocumentService - - - -@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 - - Nur ausgewählte - -``` - ---- - -## 🔲 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* diff --git a/EnvelopeGenerator.Service/MIGRATION_PLAN.md b/EnvelopeGenerator.Service/MIGRATION_PLAN.md deleted file mode 100644 index cc78fca9..00000000 --- a/EnvelopeGenerator.Service/MIGRATION_PLAN.md +++ /dev/null @@ -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` 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`. - -### 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`: - -```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`: -- `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, IOptions - // 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, IOptions - // 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 - 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( - builder.Configuration.GetSection("ServiceConfig")); - -builder.Services.AddSingleton(); -builder.Services.AddSingleton(); -builder.Services.AddSingleton(); -builder.Services.AddHostedService(); -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. - diff --git a/EnvelopeGenerator.sln b/EnvelopeGenerator.sln index d769a5cc..4b7867c0 100644 --- a/EnvelopeGenerator.sln +++ b/EnvelopeGenerator.sln @@ -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}" diff --git a/fix-report-label-read-and-confirmed.md b/fix-report-label-read-and-confirmed.md deleted file mode 100644 index 323f55fb..00000000 --- a/fix-report-label-read-and-confirmed.md +++ /dev/null @@ -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 `` block inserted **directly after** the existing -`DocumentSigned` entry: - -```xml - - Gelesen und bestätigt - - Read and confirmed - -``` - -`Model.Designer.cs` also needs a new strongly-typed property added after -`DocumentSigned`: - -```csharp -/// -/// Looks up a localized string similar to Gelesen und bestätigt. -/// -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"** diff --git a/fix-signature-field-formatting.md b/fix-signature-field-formatting.md deleted file mode 100644 index b411dc22..00000000 --- a/fix-signature-field-formatting.md +++ /dev/null @@ -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.