Files
EnvelopeGenerator/EnvelopeGenerator.Server/SF-74-PLAN.md
TekH 46c7c31e03 Add customer logo area to action bar
Introduced a customer logo section in `EnvelopeSenderPage.razor` that dynamically displays a customer logo or a placeholder when no logo is configured. The logo is fetched from the `CustomImagesService` and styled using new CSS classes in `sender-page.css`.

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

The implementation is modular and deployment-specific, allowing logo paths and styles to be updated via configuration without code changes.
2026-09-06 10:50:05 +02:00

416 lines
14 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# SF-74 — signFLOW: Theme, Dark Mode, Grid Persistence, Logo & Dashboard
**Ticket:** SF-74 | **Bearbeiter:** Hakan Tek | **Status:** In Bearbeitung
**Letzte Aktualisierung:** 2026-09-03
**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)
| 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]`) | ✅ |
**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*