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.
This commit is contained in:
2026-09-06 10:50:05 +02:00
parent 8f6db435bb
commit 46c7c31e03
3 changed files with 462 additions and 1 deletions

View File

@@ -41,6 +41,22 @@
<div class="sender-title">Umschlag-Übersicht</div> <div class="sender-title">Umschlag-Übersicht</div>
</div> </div>
@* ── Kunden-Logo (Mitte) ───────────────────────────────────────── *@
<div class="sender-customer-logo-area">
@if (_companyLogo is not null && !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>
@* ── Toolbar (rechts) ─────────────────────────────────────────── *@
<div class="sender-toolbar"> <div class="sender-toolbar">
<button class="sender-btn sender-btn--primary" @onclick="CreateEnvelope" title="Neuen Umschlag erstellen"> <button class="sender-btn sender-btn--primary" @onclick="CreateEnvelope" title="Neuen Umschlag erstellen">
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" viewBox="0 0 16 16"> <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" viewBox="0 0 16 16">
@@ -355,6 +371,7 @@
// Logo config loaded from /api/CustomImages after auth // Logo config loaded from /api/CustomImages after auth
private EnvelopeGenerator.Server.Client.Services.CustomImageDto? _appLogo; private EnvelopeGenerator.Server.Client.Services.CustomImageDto? _appLogo;
private EnvelopeGenerator.Server.Client.Services.CustomImageDto? _companyLogo;
// Stable keys used to store/restore each grid's layout in UserPreferences.GridLayouts // Stable keys used to store/restore each grid's layout in UserPreferences.GridLayouts
private const string GridKeyActive = "sender.active-envelopes"; private const string GridKeyActive = "sender.active-envelopes";
@@ -377,7 +394,8 @@
// Load logo config — authenticated, so this should succeed // Load logo config — authenticated, so this should succeed
var logos = await CustomImagesService.GetAsync(); var logos = await CustomImagesService.GetAsync();
_appLogo = logos.App; _appLogo = logos.App;
_companyLogo = logos.Company;
await LoadEnvelopesAsync(); await LoadEnvelopesAsync();
} }

View File

@@ -28,6 +28,34 @@
display: flex; display: flex;
align-items: center; align-items: center;
gap: 1rem; gap: 1rem;
flex-shrink: 0; /* don't shrink — logo area in the middle gets the space */
}
/* ── Customer logo area (middle of action bar) ──────────────────────────── */
.sender-customer-logo-area {
flex: 1; /* takes up remaining space between left and right */
display: flex;
justify-content: center;
align-items: center;
min-width: 0; /* allow flex child to shrink below content size */
}
/* Placeholder shown when no customer logo is configured */
.sf-customer-logo-placeholder {
display: flex;
align-items: center;
height: 28px;
min-width: 90px;
padding: 0 12px;
border: 1px dashed rgba(126, 34, 206, 0.28);
border-radius: 6px;
font-size: 0.65rem;
font-weight: 500;
color: rgba(126, 34, 206, 0.38);
white-space: nowrap;
letter-spacing: 0.03em;
text-transform: uppercase;
user-select: none;
} }
.sender-logo svg { .sender-logo svg {

View File

@@ -0,0 +1,415 @@
# 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*