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:
@@ -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*
|
||||
Reference in New Issue
Block a user