Updated the "Letzte Aktualisierung" date in SF-74-PLAN.md to reflect the latest progress. Added requirement 6d for a PDF Preview Popup using `DxPdfViewer` and marked it as completed. Provided additional context for Phase 6d, explaining the read-only behavior of envelopes after sending and the technical approach for implementing the PDF preview functionality.
420 lines
14 KiB
Markdown
420 lines
14 KiB
Markdown
# 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–6c)
|
||
|
||
| 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) | ✅ |
|
||
|
||
**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 nach Excel
|
||
|
||
### Technischer Ansatz
|
||
|
||
`DxGrid.ExportToXlsxAsync(fileName, options)` — direkt im Razor-Code, keine Backend-Implementierung nötig.
|
||
Download geht direkt an den Browser.
|
||
|
||
```csharp
|
||
async Task ExportToExcelAsync()
|
||
{
|
||
var grid = _activeTab == "active" ? _gridActive : _gridCompleted;
|
||
if (grid is null) return;
|
||
var fileName = $"Umschlaege_{DateTime.Now:yyyy-MM-dd}.xlsx";
|
||
await grid.ExportToXlsxAsync(fileName, new GridXlExportOptions());
|
||
}
|
||
```
|
||
|
||
> **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.
|
||
|
||
Button in Toolbar:
|
||
```razor
|
||
<button class="sender-btn" @onclick="ExportToExcelAsync"
|
||
title="Aktuelle Ansicht als Excel exportieren">
|
||
<!-- Excel-Icon SVG -->
|
||
Export nach Excel
|
||
</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 | Excel-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*
|