Compare commits

...

10 Commits

6 changed files with 710 additions and 53 deletions

109
CLAUDE.md Normal file
View File

@@ -0,0 +1,109 @@
# CLAUDE.md
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
## Project
**SmartReports / CertReports.Syncfusion** — ASP.NET Core 8 REST API che genera PDF di certificati finanziari strutturati, sostituendo il vecchio progetto WebForms DevExpress. Usa Syncfusion PDF (Community License) e SkiaSharp per i grafici.
Progetto unico nella solution: `CertReports.Syncfusion/`.
## Build & Run
```bash
# Restore + build
dotnet restore
dotnet build
# Run locale → https://localhost:{porta}/api/report/by-isin/{ISIN}
dotnet run --project CertReports.Syncfusion
# Docker
docker-compose up --build
# → http://localhost:5080/api/report/by-isin/{ISIN}
```
Non esistono test automatici al momento.
## Architettura
Il flusso di generazione è orchestrato da `ReportOrchestrator`:
```
HTTP (ISIN) → ReportController → ReportOrchestrator
├── CertificateDataService (4 SP → anagrafica, sottostanti, eventi, scenario)
├── AnagraficaSectionRenderer → PdfDocument (Sezione 1)
├── EventiSectionRenderer → PdfDocument (Sezione 2)
├── ScenarioSectionRenderer → PdfDocument (Sezione 3, opzionale)
├── ChartDataService (3 SP → dati grafico)
├── SkiaChartRenderer → PNG in memoria
├── ChartSectionRenderer → PdfDocument (Sezione 4)
└── PdfMergerService → byte[] → Response
```
**Aggiungere una nuova sezione PDF**: implementare `IPdfSectionRenderer`, impostare `SectionName` e `Order`, registrare in `Program.cs` come `AddScoped<IPdfSectionRenderer, NuovaSezione>()`. L'orchestratore la include automaticamente ordinandola per `Order`.
**Tema**: tutto in `PdfTheme.cs` — colori, font, layout, brush/pen. Modificare lì per aggiornare tutti i renderer.
**Palette colori principale** (stile "Ibrido elegante"):
- `AccentBlue` `#1565C0` — titoli, header tabelle, valori chiave
- `NegativeRed` `#CC0000` — valori negativi (performance, rendimento)
- `PositiveGreen` `#2E7D32` — valori positivi (performance)
- Brush corrispondenti: `AccentBlueBrush`, `NegativeRedBrush`, `PositiveGreenBrush`, `TableHeaderBrush` (alias di `AccentBlueBrush`)
## Configurazione chiave
- `appsettings.json`: connection string (`CertDb`), Syncfusion license key, CryptoSettings passphrase, cache TTL
- `appsettings.Development.json`: **non deve contenere `ConnectionStrings`**, altrimenti sovrascrive `appsettings.json`
- Connection string: usare `Data Source` + `Initial Catalog` + `Encrypt=False;` per `Microsoft.Data.SqlClient` v5
- Se la connessione fallisce via Named Pipes, aggiungere prefisso `tcp:` all'indirizzo IP
## Gotcha Syncfusion v33 & SkiaSharp
| Problema | Soluzione |
|----------|-----------|
| `Color(r,g,b)` rimosso | `Color.FromArgb(255, r, g, b)` |
| `Syncfusion.Drawing.Net.Core` non esiste | Inglobato in `Syncfusion.Pdf.Net.Core`, rimuovere dal `.csproj` |
| `PdfStandardFont` non è IDisposable | Non usare `using` sulla dichiarazione |
| `grid.Headers` non iterabile con `foreach` | `for (int r = 0; r < grid.Headers.Count; r++)` |
| `PdfTextWebLink` non trovato | Aggiungere `using Syncfusion.Pdf.Interactive;` |
| `RectangleF` non supporta named arguments | `new RectangleF(x, y, w, h)` — no `startY:` o simili |
| `grid.Draw()` restituisce `void` | Stimare altezza con `(rows + 1) * RowHeight` |
| `PdfMergerService`: stream chiuso prima del `Save()` | Tenere tutti gli stream aperti fino al salvataggio finale, poi cleanup in `finally` |
| SkiaSharp `DrawText` obsoleto | `SKFont` + `canvas.DrawText(text, x, y, SKTextAlign, font, paint)` |
| Namespace conflict `CertReports.Syncfusion.Pdf` | Aggiungere `using Syncfusion.Pdf;` esplicito nel file |
## Database
Tutte le stored procedure sono su `FirstSolutionDB`. I dati tornano **già formattati come stringhe** dalle SP (es. `FORMAT(value,'P2','it-IT')`): i modelli C# usano `string` per questi campi. Solo `NominalValue`, `PrezzoEmissione`, `CpnPagati`, `CpnDaPagare`, `CpnInMemoria` sono `decimal?` perché servono come valori numerici nel rendering.
## API Endpoints
- `GET /api/report?p={isin_cifrato}` / `?alias={id}` / `/by-isin/{isin}` — PDF inline
- `GET /api/report/download?p={...}` / `?alias={...}` — PDF come allegato
- Tutti gli endpoint report accettano `?branding=true` (default `false`) per abilitare il footer "Powered by Smart Roots"
- `GET /api/chart/{isin}[?format=png|pdf&width=&height=]` — grafico standalone
- `GET /health` — health check DB + chart service
## Footer branding
Il parametro `?branding=true` aggiunge al footer di ogni pagina del report:
- Sinistra: `"Powered by "` + hyperlink PDF cliccabile `"Smart Roots"``https://www.smart-roots.net`
- Destra: numero pagina
Con `branding=false` (default): solo numero pagina centrato.
La cache usa chiavi separate: `{isin}:branded` vs `{isin}`. Il flag è propagato come `CertificateReportData.ShowBranding` e passato a `PdfTheme.DrawFooter(g, pageWidth, pageHeight, pageNumber, showBranding)`. L'hyperlink usa `PdfTextWebLink` (richiede `using Syncfusion.Pdf.Interactive;`).
## Sezione 1 — AnagraficaSectionRenderer
Struttura a 3 sezioni verticali in una singola pagina A4:
| Sezione | Contenuto |
|---------|-----------|
| **Titolo** | "Scheda Prodotto {ISIN}" blu + Bid/Ask a destra + linea separatrice blu |
| **A — Caratteristiche Prodotto** | Tabella emittente a sinistra (ISIN, Mercato, Valuta, Date, Autocall) + tabella cedole a destra (CpnPagati/DaPagare/InMemoria, RendimentoAttuale) |
| **B — Analisi** | 8 KV a sinistra + 9 KV a destra. Leva/FattoreAirbag/TriggerOneStar mostrati come `"—"` anche se vuoti |
| **C — Sottostanti** | PdfGrid 9 colonne: Nome, Strike, Last, % Perf., Barr.K, Buffer K, Trig.CPN, Buf.CPN, Trig.AC (Dist.AC rimossa) |
Se la tabella Sottostanti non entra (y > PageH-80pt), si crea una nuova pagina. Il footer viene disegnato su ogni pagina con `PdfTheme.DrawFooter`.

View File

@@ -31,7 +31,15 @@ public static class PdfTheme
public static readonly Color SeparatorLine = Color.FromArgb(255, 221, 221, 221); // #DDDDDD public static readonly Color SeparatorLine = Color.FromArgb(255, 221, 221, 221); // #DDDDDD
public static readonly Color TableBorder = Color.FromArgb(255, 187, 187, 187); // #BBBBBB public static readonly Color TableBorder = Color.FromArgb(255, 187, 187, 187); // #BBBBBB
public static readonly Color TableAltRow = Color.FromArgb(255, 247, 249, 252); // #F7F9FC public static readonly Color TableAltRow = Color.FromArgb(255, 247, 249, 252); // #F7F9FC
public static readonly Color TableHeaderBg = Color.FromArgb(255, 21, 101, 192); // #1565C0 // TableHeaderBg is AccentBlue — use AccentBlueBrush / TableHeaderBrush directly
// Colori box header pagina 1
public static readonly Color AccentBlueDark = Color.FromArgb(255, 13, 71, 161); // #0D47A1
public static readonly Color BoxLightBlueBg = Color.FromArgb(255, 235, 242, 251); // #EBF2FB
public static readonly Color BoxGrayBg = Color.FromArgb(255, 245, 245, 245); // #F5F5F5
public static readonly Color BoxGrayAccent = Color.FromArgb(255, 136, 136, 136); // #888888
public static readonly Color BoxGrayLabel = Color.FromArgb(255, 102, 102, 102); // #666666
public static readonly Color BoxGrayValue = Color.FromArgb(255, 51, 51, 51); // #333333
// ─── Font ────────────────────────────────────────────────────────── // ─── Font ──────────────────────────────────────────────────────────
private static readonly PdfStandardFont _fontRegular = new(PdfFontFamily.Helvetica, 8f); private static readonly PdfStandardFont _fontRegular = new(PdfFontFamily.Helvetica, 8f);
@@ -75,21 +83,27 @@ public static class PdfTheme
public static PdfBrush PositiveBrush => new PdfSolidBrush(PositiveValue); public static PdfBrush PositiveBrush => new PdfSolidBrush(PositiveValue);
public static PdfBrush NegativeBrush => new PdfSolidBrush(NegativeValue); public static PdfBrush NegativeBrush => new PdfSolidBrush(NegativeValue);
public static PdfPen BorderPen => new PdfPen(BorderColor, 0.5f); public static PdfPen BorderPen => new PdfPen(BorderColor, 0.5f);
public static PdfBrush AccentBlueBrush => new PdfSolidBrush(AccentBlue); public static PdfBrush AccentBlueBrush => new PdfSolidBrush(AccentBlue);
public static PdfBrush NegativeBrush2 => new PdfSolidBrush(NegativeRed); public static PdfBrush NegativeRedBrush => new PdfSolidBrush(NegativeRed);
public static PdfBrush PositiveBrush2 => new PdfSolidBrush(PositiveGreen); public static PdfBrush PositiveGreenBrush => new PdfSolidBrush(PositiveGreen);
public static PdfBrush FooterTextBrush => new PdfSolidBrush(FooterText); public static PdfBrush FooterTextBrush => new PdfSolidBrush(FooterText);
public static PdfBrush TableAltRowBrush => new PdfSolidBrush(TableAltRow); public static PdfBrush TableAltRowBrush => new PdfSolidBrush(TableAltRow);
public static PdfBrush TableHeaderBrush => new PdfSolidBrush(TableHeaderBg); public static PdfBrush TableHeaderBrush => AccentBlueBrush;
public static PdfPen SeparatorPen => new PdfPen(SeparatorLine, 0.5f); public static PdfPen SeparatorPen => new PdfPen(SeparatorLine, 0.5f);
public static PdfPen TableBorderPen => new PdfPen(TableBorder, 0.5f); public static PdfPen TableBorderPen => new PdfPen(TableBorder, 0.5f);
public static PdfPen AccentBluePen => new PdfPen(AccentBlue, 2.5f); public static PdfPen AccentBluePen => new PdfPen(AccentBlue, 2.5f);
public static PdfBrush AccentBlueDarkBrush => new PdfSolidBrush(AccentBlueDark);
public static PdfBrush BoxLightBlueBgBrush => new PdfSolidBrush(BoxLightBlueBg);
public static PdfBrush BoxGrayBgBrush => new PdfSolidBrush(BoxGrayBg);
public static PdfBrush BoxGrayAccentBrush => new PdfSolidBrush(BoxGrayAccent);
public static PdfBrush BoxGrayLabelBrush => new PdfSolidBrush(BoxGrayLabel);
public static PdfBrush BoxGrayValueBrush => new PdfSolidBrush(BoxGrayValue);
// ─── Utility ─────────────────────────────────────────────────────── // ─── Utility ───────────────────────────────────────────────────────
public static PdfBrush ValueBrush(decimal? value) public static PdfBrush ValueBrush(decimal? value)
{ {
if (value == null) return TextBrush; if (value == null) return TextBrush;
return value >= 0 ? PositiveBrush : NegativeBrush; return value >= 0 ? PositiveGreenBrush : NegativeRedBrush;
} }
/// <summary> /// <summary>
@@ -200,7 +214,7 @@ public static class PdfTheme
Font = Footer, Font = Footer,
Brush = AccentBlueBrush, Brush = AccentBlueBrush,
}; };
webLink.DrawTextWebLink(g, new PointF(prefixWidth, footerY)); webLink.DrawTextWebLink(g, new PointF(prefixWidth + 2f, footerY));
// Numero pagina a destra // Numero pagina a destra
g.DrawString(pageLabel, Footer, FooterTextBrush, g.DrawString(pageLabel, Footer, FooterTextBrush,

View File

@@ -68,46 +68,79 @@ public class AnagraficaSectionRenderer : IPdfSectionRenderer
} }
// ═══════════════════════════════════════════════════════════════ // ═══════════════════════════════════════════════════════════════
// TITOLO con Bid/Ask a destra // TITOLO centrato + riga box informativi (Tipologia/Data/Bid/Ask)
// ═══════════════════════════════════════════════════════════════ // ═══════════════════════════════════════════════════════════════
private float DrawTitle(PdfGraphics g, CertificateInfo info, float w, float y) private float DrawTitle(PdfGraphics g, CertificateInfo info, float w, float y)
{ {
// ISIN + titolo a sinistra // ── Titolo centrato ──────────────────────────────────────────
g.DrawString($"Scheda Prodotto {info.Isin}", g.DrawString($"Scheda Prodotto {info.Isin}",
PdfTheme.SectionTitleFont, PdfTheme.SectionTitleFont,
new PdfSolidBrush(PdfTheme.AccentBlue), new PdfSolidBrush(PdfTheme.AccentBlue),
new RectangleF(0, y, w * 0.65f, 20f)); new RectangleF(0, y, w, 20f),
new PdfStringFormat(PdfTextAlignment.Center));
// Bid/Ask a destra y += 24f;
if (!string.IsNullOrEmpty(info.Bid) && !string.IsNullOrEmpty(info.Ask))
{
string bidAsk = $"{info.Bid} BID {info.Ask} ASK";
g.DrawString(info.LastPriceDate, PdfTheme.Small,
new PdfSolidBrush(PdfTheme.TextSecondary),
new RectangleF(w * 0.65f, y, w * 0.35f, 10f),
new PdfStringFormat(PdfTextAlignment.Right));
g.DrawString(bidAsk, PdfTheme.Bold,
new PdfSolidBrush(PdfTheme.AccentBlue),
new RectangleF(w * 0.65f, y + 10f, w * 0.35f, 12f),
new PdfStringFormat(PdfTextAlignment.Right));
}
y += 22f;
// Tipologia sotto il titolo
if (!string.IsNullOrEmpty(info.Categoria))
{
g.DrawString($"Tipologia: {info.Categoria}", PdfTheme.Regular,
new PdfSolidBrush(PdfTheme.TextSecondary),
new RectangleF(0, y, w, 12f));
y += 13f;
}
// Linea separatrice blu // Linea separatrice blu
g.DrawLine(PdfTheme.AccentBluePen, 0, y + 2f, w, y + 2f); g.DrawLine(PdfTheme.AccentBluePen, 0, y, w, y);
y += 8f; y += 8f;
// ── Riga box: Tipologia | Data | Bid | Ask ───────────────────
const float boxH = 28f;
const float gap = 6f;
const float dataW = 84f;
const float bidW = 70f;
const float askW = 70f;
bool showTip = !string.IsNullOrEmpty(info.Categoria);
bool showData = !string.IsNullOrEmpty(info.LastPriceDate);
bool showBidAsk = !string.IsNullOrEmpty(info.Bid) && !string.IsNullOrEmpty(info.Ask);
// Calcolo dinamico larghezza Tipologia in base ai box presenti
int nBoxes = (showTip ? 1 : 0) + (showData ? 1 : 0) + (showBidAsk ? 2 : 0);
float totalGaps = Math.Max(0, nBoxes - 1) * gap;
float fixedW = (showData ? dataW : 0f) + (showBidAsk ? bidW + askW : 0f);
float tipW = showTip ? (w - totalGaps - fixedW) : 0f;
float xCursor = 0f;
if (showTip)
{
DrawInfoBox(g, xCursor, y, tipW, boxH,
"TIPOLOGIA", info.Categoria,
PdfTheme.BoxLightBlueBgBrush, PdfTheme.AccentBlueBrush,
PdfTheme.AccentBlueBrush, PdfTheme.AccentBlueDarkBrush,
PdfTheme.Bold);
xCursor += tipW + gap;
}
if (showData)
{
DrawInfoBox(g, xCursor, y, dataW, boxH,
"DATA", info.LastPriceDate,
PdfTheme.BoxGrayBgBrush, PdfTheme.BoxGrayAccentBrush,
PdfTheme.BoxGrayLabelBrush, PdfTheme.BoxGrayValueBrush,
PdfTheme.Bold);
xCursor += dataW + gap;
}
if (showBidAsk)
{
DrawInfoBox(g, xCursor, y, bidW, boxH,
"BID", info.Bid,
PdfTheme.BoxLightBlueBgBrush, PdfTheme.AccentBlueBrush,
PdfTheme.AccentBlueBrush, PdfTheme.AccentBlueDarkBrush,
PdfTheme.Bold);
xCursor += bidW + gap;
DrawInfoBox(g, xCursor, y, askW, boxH,
"ASK", info.Ask,
PdfTheme.BoxLightBlueBgBrush, PdfTheme.AccentBlueBrush,
PdfTheme.AccentBlueBrush, PdfTheme.AccentBlueDarkBrush,
PdfTheme.Bold);
}
y += boxH + 10f;
return y; return y;
} }
@@ -125,6 +158,28 @@ public class AnagraficaSectionRenderer : IPdfSectionRenderer
return y + 16f; return y + 16f;
} }
// ═══════════════════════════════════════════════════════════════
// INFO BOX (box colorato con accent line laterale)
// ═══════════════════════════════════════════════════════════════
private void DrawInfoBox(
PdfGraphics g,
float x, float y, float w, float boxH,
string label, string value,
PdfBrush bgBrush, PdfBrush accentBrush,
PdfBrush labelBrush, PdfBrush valueBrush,
PdfFont valueFont)
{
g.DrawRectangle(bgBrush, new RectangleF(x, y, w, boxH));
g.DrawRectangle(accentBrush, new RectangleF(x, y, 4f, boxH));
float innerX = x + 4f + 6f;
float innerW = w - 4f - 6f - 4f;
g.DrawString(label, PdfTheme.Small, labelBrush,
new RectangleF(innerX, y + 4f, innerW, 10f));
g.DrawString(value, valueFont, valueBrush,
new RectangleF(innerX, y + 14f, innerW, 14f));
}
// ═══════════════════════════════════════════════════════════════ // ═══════════════════════════════════════════════════════════════
// SEZIONE A: CARATTERISTICHE PRODOTTO // SEZIONE A: CARATTERISTICHE PRODOTTO
// Sinistra: tabella emittente | Destra: cedole + rendimento totale // Sinistra: tabella emittente | Destra: cedole + rendimento totale
@@ -262,25 +317,25 @@ public class AnagraficaSectionRenderer : IPdfSectionRenderer
{ {
var leftItems = new (string Label, string Value)[] var leftItems = new (string Label, string Value)[]
{ {
("Importo Cedola (p.a.)", info.NominalAnnualYield), ("Importo Cedola (p.a.)", info.NominalAnnualYield ?? "-"),
("Frequenza Cedola", info.FrequenzaCedole), ("Frequenza Cedola", info.FrequenzaCedole ?? "-"),
("Valore Nominale", info.NominalValue?.ToString("N0") ?? "-"), ("Valore Nominale", info.NominalValue?.ToString("N0") ?? "-"),
("Prezzo Emissione", info.PrezzoEmissione?.ToString("N0") ?? "-"), ("Prezzo Emissione", info.PrezzoEmissione?.ToString("N0") ?? "-"),
("Barriera Capitale", info.LivelloBarriera), ("Barriera Capitale", info.LivelloBarriera ?? "-"),
("Tipo Barriera", info.BarrierType), ("Tipo Barriera", info.BarrierType ?? "-"),
("Tipo Basket", info.BasketType), ("Tipo Basket", info.BasketType ?? "-"),
("Leva", string.IsNullOrWhiteSpace(info.Leva) ? "—" : info.Leva), ("Leva", string.IsNullOrWhiteSpace(info.Leva) ? "—" : info.Leva),
}; };
var rightItems = new (string Label, string Value)[] var rightItems = new (string Label, string Value)[]
{ {
("Rend. Capitale a Scadenza", info.CapitalReturnAtMaturity), ("Rend. Capitale a Scadenza", info.CapitalReturnAtMaturity ?? "-"),
("IRR", info.IRR), ("IRR", info.IRR ?? "-"),
("Protezione Capitale", info.BufferKProt), ("Protezione Capitale", info.BufferKProt ?? "-"),
("Protezione Coupon", info.BufferCPNProt), ("Protezione Coupon", info.BufferCPNProt ?? "-"),
("Valore Autocall", info.AutocallValue), ("Valore Autocall", info.AutocallValue ?? "-"),
("Distanza Autocall", info.TriggerAutocallDistance), ("Distanza Autocall", info.TriggerAutocallDistance ?? "-"),
("Rendimento Autocall", info.AutocallReturn), ("Rendimento Autocall", info.AutocallReturn ?? "-"),
("Fattore Airbag", string.IsNullOrWhiteSpace(info.FattoreAirbag) ? "—" : info.FattoreAirbag), ("Fattore Airbag", string.IsNullOrWhiteSpace(info.FattoreAirbag) ? "—" : info.FattoreAirbag),
("Trigger OneStar", string.IsNullOrWhiteSpace(info.TriggerOneStar) ? "—" : info.TriggerOneStar), ("Trigger OneStar", string.IsNullOrWhiteSpace(info.TriggerOneStar) ? "—" : info.TriggerOneStar),
}; };
@@ -335,8 +390,8 @@ public class AnagraficaSectionRenderer : IPdfSectionRenderer
grid.Style.CellPadding = new PdfPaddings(2, 2, 2, 2); grid.Style.CellPadding = new PdfPaddings(2, 2, 2, 2);
// 9 colonne // 9 colonne
string[] headers = { "Nome", "Strike", "Last", "% Perf.", "Barr.K", string[] headers = { "Nome", "Strike", "Last", "% Perf.", "Barriera Capitale",
"Buffer K", "Trig.CPN", "Buf.CPN", "Trig.AC" }; "Buffer Capitale", "Trigger Cedola", "Buffer Cedola", "Trigger Autocall" };
foreach (var _ in headers) grid.Columns.Add(); foreach (var _ in headers) grid.Columns.Add();

View File

@@ -42,7 +42,7 @@ public class EventiSectionRenderer : IPdfSectionRenderer
string[] headers = string[] headers =
{ {
"Osservazione", "Ex Date", "Record", "Pagamento", "Osservazione", "Ex Date", "Record", "Pagamento",
"Trigger CPN", "Cedola %", "Pagato", "Memoria", "Trigger Cedola", "Cedola %", "Pagato", "Memoria",
"Importo Pagato", "Trigger Autocall", "Valore Autocall" "Importo Pagato", "Trigger Autocall", "Valore Autocall"
}; };

View File

@@ -0,0 +1,303 @@
# Page 1 Header Restyle & Label Fixes — Implementation Plan
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
**Goal:** Redesign l'header di pagina 1 (titolo centrato + box Tipologia/Data/Bid/Ask uniformi), rinominare colonne nelle tabelle Sottostanti ed Eventi, e fixare lo spazio nel footer branding.
**Architecture:** Modifiche localizzate a 3 file esistenti. Nessuna nuova classe o interfaccia. Le nuove costanti colore/brush vengono aggiunte a `PdfTheme.cs` seguendo il pattern `=>` già usato nel file. Il nuovo `DrawTitle` in `AnagraficaSectionRenderer` è una riscrittura completa del metodo esistente.
**Tech Stack:** C# / ASP.NET Core 8, Syncfusion PDF v33, Syncfusion.Drawing
---
## File map
| File | Modifica |
|------|----------|
| `CertReports.Syncfusion/Helpers/PdfTheme.cs` | Aggiungi 6 colori + 5 brush (stile `=>`) + fix footer offset (+2pt) |
| `CertReports.Syncfusion/Services/Implementations/AnagraficaSectionRenderer.cs` | Riscrivi `DrawTitle`, aggiungi `DrawInfoBox`, rinomina headers sottostanti |
| `CertReports.Syncfusion/Services/Implementations/EventiSectionRenderer.cs` | Rinomina "Trigger CPN" → "Trigger Cedola" |
---
## Task 1: Nuovi colori e fix footer in PdfTheme.cs
**Files:**
- Modify: `CertReports.Syncfusion/Helpers/PdfTheme.cs`
- [ ] **Step 1: Aggiungi nuove costanti colore**
Inserisci dopo la riga `public static readonly Color TableAltRow = ...` (blocco colori, riga ~33):
```csharp
// Colori box header pagina 1
public static readonly Color AccentBlueDark = Color.FromArgb(255, 13, 71, 161); // #0D47A1
public static readonly Color BoxLightBlueBg = Color.FromArgb(255, 235, 242, 251); // #EBF2FB
public static readonly Color BoxGrayBg = Color.FromArgb(255, 245, 245, 245); // #F5F5F5
public static readonly Color BoxGrayAccent = Color.FromArgb(255, 136, 136, 136); // #888888
public static readonly Color BoxGrayLabel = Color.FromArgb(255, 102, 102, 102); // #666666
public static readonly Color BoxGrayValue = Color.FromArgb(255, 51, 51, 51); // #333333
```
- [ ] **Step 2: Aggiungi i brush corrispondenti — usa il pattern `=>` come il resto del file**
Inserisci dopo la riga `public static PdfPen AccentBluePen => ...` (blocco Brushes, riga ~86):
```csharp
public static PdfBrush AccentBlueDarkBrush => new PdfSolidBrush(AccentBlueDark);
public static PdfBrush BoxLightBlueBgBrush => new PdfSolidBrush(BoxLightBlueBg);
public static PdfBrush BoxGrayBgBrush => new PdfSolidBrush(BoxGrayBg);
public static PdfBrush BoxGrayAccentBrush => new PdfSolidBrush(BoxGrayAccent);
public static PdfBrush BoxGrayLabelBrush => new PdfSolidBrush(BoxGrayLabel);
public static PdfBrush BoxGrayValueBrush => new PdfSolidBrush(BoxGrayValue);
```
- [ ] **Step 3: Fix footer — cambia solo l'argomento `PointF` in `DrawTextWebLink` (riga ~203)**
```csharp
// PRIMA:
webLink.DrawTextWebLink(g, new PointF(prefixWidth, footerY));
// DOPO:
webLink.DrawTextWebLink(g, new PointF(prefixWidth + 2f, footerY));
```
> Il `DrawString` del testo prefisso (riga ~193) usa già `prefixWidth + 2f` come larghezza del `RectangleF` — questo fix allinea solo il punto di partenza del link.
- [ ] **Step 4: Build**
```bash
dotnet build CertReports.Syncfusion/CertReports.Syncfusion.csproj
```
Atteso: `Build succeeded` senza errori.
- [ ] **Step 5: Commit**
```bash
git add CertReports.Syncfusion/Helpers/PdfTheme.cs
git commit -m "feat: add box header colors/brushes and fix footer branding offset"
```
---
## Task 2: Riscrittura DrawTitle in AnagraficaSectionRenderer.cs
**Files:**
- Modify: `CertReports.Syncfusion/Services/Implementations/AnagraficaSectionRenderer.cs`
Il metodo `DrawTitle` (righe 74-112) viene sostituito interamente. Si aggiunge il metodo privato `DrawInfoBox`.
**Layout:**
```
Scheda Prodotto XS1234567890 ← centrato, AccentBlue, SectionTitleFont
───────────────────────────────────────── ← AccentBluePen
[TIPOLOGIA / Cert. Bonus Cap] [DATA / 18/03] [BID / 98.50] [ASK / 99.20]
```
**Dimensioni box:**
- Altezza: `28f`; Gap: `6f`; Larghezza Data: `84f`, Bid: `70f`, Ask: `70f`
- `tipW` calcolato dinamicamente (vedi DrawTitle)
- Bordo sinistro accent: `4f`; Padding: `6f` sx, `4f` dx
- Label: `PdfTheme.Small` (6.5pt) a `y + 4f`
- Valore Bid/Ask/Data: `PdfTheme.Bold` (8pt bold) a `y + 14f`
- Valore Tipologia: `PdfTheme.Bold` (8pt bold) a `y + 14f` — testo più lungo, dimensione coerente con gli altri box
- [ ] **Step 1: Aggiungi il metodo `DrawInfoBox` alla classe (prima di `DrawCaratteristiche`)**
```csharp
private void DrawInfoBox(
PdfGraphics g,
float x, float y, float w, float boxH,
string label, string value,
PdfBrush bgBrush, PdfBrush accentBrush,
PdfBrush labelBrush, PdfBrush valueBrush,
PdfFont valueFont)
{
g.DrawRectangle(bgBrush, new RectangleF(x, y, w, boxH));
g.DrawRectangle(accentBrush, new RectangleF(x, y, 4f, boxH));
float innerX = x + 4f + 6f;
float innerW = w - 4f - 6f - 4f;
g.DrawString(label, PdfTheme.Small, labelBrush,
new RectangleF(innerX, y + 4f, innerW, 10f));
g.DrawString(value, valueFont, valueBrush,
new RectangleF(innerX, y + 14f, innerW, 14f));
}
```
> **Nota Syncfusion:** `RectangleF` non supporta named arguments — sempre forma posizionale `new RectangleF(x, y, w, h)`.
- [ ] **Step 2: Sostituisci il metodo `DrawTitle` (righe 74-112) con la nuova implementazione**
```csharp
private float DrawTitle(PdfGraphics g, CertificateInfo info, float w, float y)
{
// ── Titolo centrato ──────────────────────────────────────────
g.DrawString($"Scheda Prodotto {info.Isin}",
PdfTheme.SectionTitleFont,
new PdfSolidBrush(PdfTheme.AccentBlue),
new RectangleF(0, y, w, 20f),
new PdfStringFormat(PdfTextAlignment.Center));
y += 24f;
// Linea separatrice blu
g.DrawLine(PdfTheme.AccentBluePen, 0, y, w, y);
y += 8f;
// ── Riga box: Tipologia | Data | Bid | Ask ───────────────────
const float boxH = 28f;
const float gap = 6f;
const float dataW = 84f;
const float bidW = 70f;
const float askW = 70f;
bool showTip = !string.IsNullOrEmpty(info.Categoria);
bool showData = !string.IsNullOrEmpty(info.LastPriceDate);
bool showBidAsk = !string.IsNullOrEmpty(info.Bid) && !string.IsNullOrEmpty(info.Ask);
// Calcolo dinamico larghezza Tipologia in base ai box presenti
int nBoxes = (showTip ? 1 : 0) + (showData ? 1 : 0) + (showBidAsk ? 2 : 0);
float totalGaps = Math.Max(0, nBoxes - 1) * gap;
float fixedW = (showData ? dataW : 0f) + (showBidAsk ? bidW + askW : 0f);
float tipW = showTip ? (w - totalGaps - fixedW) : 0f;
float xCursor = 0f;
if (showTip)
{
DrawInfoBox(g, xCursor, y, tipW, boxH,
"TIPOLOGIA", info.Categoria,
PdfTheme.BoxLightBlueBgBrush, PdfTheme.AccentBlueBrush,
PdfTheme.AccentBlueBrush, PdfTheme.AccentBlueDarkBrush,
PdfTheme.Bold);
xCursor += tipW + gap;
}
if (showData)
{
DrawInfoBox(g, xCursor, y, dataW, boxH,
"DATA", info.LastPriceDate,
PdfTheme.BoxGrayBgBrush, PdfTheme.BoxGrayAccentBrush,
PdfTheme.BoxGrayLabelBrush, PdfTheme.BoxGrayValueBrush,
PdfTheme.Bold);
xCursor += dataW + gap;
}
if (showBidAsk)
{
DrawInfoBox(g, xCursor, y, bidW, boxH,
"BID", info.Bid,
PdfTheme.BoxLightBlueBgBrush, PdfTheme.AccentBlueBrush,
PdfTheme.AccentBlueBrush, PdfTheme.AccentBlueDarkBrush,
PdfTheme.Bold);
xCursor += bidW + gap;
DrawInfoBox(g, xCursor, y, askW, boxH,
"ASK", info.Ask,
PdfTheme.BoxLightBlueBgBrush, PdfTheme.AccentBlueBrush,
PdfTheme.AccentBlueBrush, PdfTheme.AccentBlueDarkBrush,
PdfTheme.Bold);
}
y += boxH + 10f;
return y;
}
```
- [ ] **Step 3: Build**
```bash
dotnet build CertReports.Syncfusion/CertReports.Syncfusion.csproj
```
Atteso: `Build succeeded`.
- [ ] **Step 4: Verifica visiva**
Avvia il progetto e apri nel browser:
```
https://localhost:{porta}/api/report/by-isin/{ISIN_VALIDO}
```
Controlla che la pagina 1 mostri: titolo centrato + linea + riga box con altezze uniformi.
Con `?branding=true` verifica che "Powered by Smart Roots" abbia lo spazio corretto tra "by" e il link.
- [ ] **Step 5: Commit**
```bash
git add CertReports.Syncfusion/Services/Implementations/AnagraficaSectionRenderer.cs
git commit -m "feat: redesign page1 header - centered title, info boxes row (Tipologia/Data/Bid/Ask)"
```
---
## Task 3: Rinomina colonne tabella Sottostanti
**Files:**
- Modify: `CertReports.Syncfusion/Services/Implementations/AnagraficaSectionRenderer.cs` (metodo `DrawSottostanti`, riga ~338)
- [ ] **Step 1: Aggiorna l'array `headers` in `DrawSottostanti`**
```csharp
// PRIMA:
string[] headers = { "Nome", "Strike", "Last", "% Perf.", "Barr.K",
"Buffer K", "Trig.CPN", "Buf.CPN", "Trig.AC" };
// DOPO:
string[] headers = { "Nome", "Strike", "Last", "% Perf.", "Barriera Capitale",
"Buffer Capitale", "Trigger Cedola", "Buffer Cedola", "Trig.AC" };
```
- [ ] **Step 2: Build**
```bash
dotnet build CertReports.Syncfusion/CertReports.Syncfusion.csproj
```
- [ ] **Step 3: Commit**
```bash
git add CertReports.Syncfusion/Services/Implementations/AnagraficaSectionRenderer.cs
git commit -m "fix: rename sottostanti column headers (Barriera/Buffer/Trigger Cedola)"
```
---
## Task 4: Rinomina colonna tabella Eventi
**Files:**
- Modify: `CertReports.Syncfusion/Services/Implementations/EventiSectionRenderer.cs` (riga ~45)
- [ ] **Step 1: Aggiorna l'array `headers` in `EventiSectionRenderer.Render`**
```csharp
// PRIMA:
"Trigger CPN", "Cedola %", "Pagato", "Memoria",
// DOPO:
"Trigger Cedola", "Cedola %", "Pagato", "Memoria",
```
- [ ] **Step 2: Build**
```bash
dotnet build CertReports.Syncfusion/CertReports.Syncfusion.csproj
```
- [ ] **Step 3: Commit**
```bash
git add CertReports.Syncfusion/Services/Implementations/EventiSectionRenderer.cs
git commit -m "fix: rename eventi column Trigger CPN -> Trigger Cedola"
```
---
## Verifica finale
- [ ] Avvia il server: `dotnet run --project CertReports.Syncfusion`
- [ ] Apri report con un ISIN valido, verifica pagina 1:
- Titolo "Scheda Prodotto {ISIN}" centrato in blu
- Linea separatrice blu
- Box Tipologia / Data / Bid / Ask sulla stessa riga, stessa altezza (28pt)
- Box Tipologia si estende per occupare lo spazio residuo
- [ ] Verifica tabella Sottostanti: header con "Barriera Capitale", "Buffer Capitale", "Trigger Cedola", "Buffer Cedola"
- [ ] Verifica sezione Eventi: colonna "Trigger Cedola" (era "Trigger CPN")
- [ ] Verifica footer con `?branding=true`: "Powered by Smart Roots" con spazio visibile tra "by" e il link cliccabile

View File

@@ -0,0 +1,176 @@
# Design Spec: Page 1 Header Restyle & Label Fixes
**Date:** 2026-03-20
**Status:** Approved
**Scope:** `AnagraficaSectionRenderer.cs`, `EventiSectionRenderer.cs`, `PdfTheme.cs`
---
## Overview
Piccolo insieme di modifiche visive e testuali alla pagina 1 del report PDF e alla sezione eventi, senza alterare la struttura generale del report.
---
## 1. Nuovo layout header Pagina 1 (`AnagraficaSectionRenderer.DrawTitle`)
### Layout approvato
```
Scheda Prodotto XS1234567890 ← centrato, blu bold, font grande
──────────────────────────────────────────────── ← linea separatrice AccentBlue (2pt)
[TIPOLOGIA / Cert. Bonus Cap] [DATA / 18/03/26] [BID / 98.50] [ASK / 99.20]
```
### Specifiche riquadri (riga inferiore)
Tutti i box hanno la **stessa altezza** (28pt) e la **stessa struttura a due righe**:
- Riga 1: label piccola (9pt, uppercase, bold, colore accent), altezza 11pt
- Riga 2: valore (13pt bold, colore accent scuro), altezza 17pt
Il padding interno è 5pt verticale, 10pt orizzontale (dopo il bordo sinistro da 4pt).
| Box | Larghezza | Sfondo | Bordo sinistro | Colore label | Colore valore |
|-----|-----------|--------|----------------|--------------|---------------|
| Tipologia | `PageW - 3*6 - 84 - 70 - 70` | `PdfTheme.BoxLightBlueBg` | `PdfTheme.AccentBlue` 4pt | `PdfTheme.AccentBlue` | `PdfTheme.AccentBlueDark` |
| Data | 84pt fisso | `PdfTheme.BoxGrayBg` | `PdfTheme.BoxGrayAccent` 4pt | `PdfTheme.BoxGrayLabel` | `PdfTheme.BoxGrayValue` |
| Bid | 70pt fisso | `PdfTheme.BoxLightBlueBg` | `PdfTheme.AccentBlue` 4pt | `PdfTheme.AccentBlue` | `PdfTheme.AccentBlueDark` |
| Ask | 70pt fisso | `PdfTheme.BoxLightBlueBg` | `PdfTheme.AccentBlue` 4pt | `PdfTheme.AccentBlue` | `PdfTheme.AccentBlueDark` |
Gap tra box: 6pt.
### Nuove costanti da aggiungere a `PdfTheme.cs`
```csharp
public static readonly Color AccentBlueDark = Color.FromArgb(255, 13, 71, 161); // #0D47A1
public static readonly Color BoxLightBlueBg = Color.FromArgb(255,235,242,251); // #EBF2FB
public static readonly Color BoxGrayBg = Color.FromArgb(255,245,245,245); // #F5F5F5
public static readonly Color BoxGrayAccent = Color.FromArgb(255,136,136,136); // #888888
public static readonly Color BoxGrayLabel = Color.FromArgb(255,102,102,102); // #666666
public static readonly Color BoxGrayValue = Color.FromArgb(255, 51, 51, 51); // #333333
// Brush corrispondenti
public static readonly PdfBrush BoxLightBlueBgBrush = new PdfSolidBrush(BoxLightBlueBg);
public static readonly PdfBrush BoxGrayBgBrush = new PdfSolidBrush(BoxGrayBg);
public static readonly PdfBrush BoxGrayAccentBrush = new PdfSolidBrush(BoxGrayAccent);
public static readonly PdfBrush BoxGrayLabelBrush = new PdfSolidBrush(BoxGrayLabel);
public static readonly PdfBrush BoxGrayValueBrush = new PdfSolidBrush(BoxGrayValue);
public static readonly PdfBrush AccentBlueDarkBrush = new PdfSolidBrush(AccentBlueDark);
```
### Font per i box
Riusare i font esistenti in `PdfTheme`:
- Label: `PdfTheme.Small` (9pt) — già esistente
- Valore Bid/Ask/Data: `PdfTheme.Bold` (stesso usato altrove)
- Valore Tipologia: nuovo font `PdfTheme.BoldSmall` (11pt bold) — aggiungere se non esistente, altrimenti usare font bold a 11pt inline
### Implementazione PDF (Syncfusion) — helper `DrawInfoBox`
Creare un metodo privato in `AnagraficaSectionRenderer`:
```csharp
private void DrawInfoBox(PdfGraphics g, float x, float y, float w, float boxH,
string label, string value,
PdfBrush bgBrush, PdfBrush accentBrush, PdfBrush labelBrush, PdfBrush valueBrush,
PdfFont valueFont)
{
// Sfondo
g.DrawRectangle(bgBrush, new RectangleF(x, y, w, boxH));
// Bordo sinistro 4pt
g.DrawRectangle(accentBrush, new RectangleF(x, y, 4f, boxH));
// Label (riga 1) — parte dopo il bordo + padding
float innerX = x + 4f + 6f;
float innerW = w - 4f - 6f - 4f;
g.DrawString(label, PdfTheme.Small, labelBrush,
new RectangleF(innerX, y + 4f, innerW, 11f));
// Valore (riga 2)
g.DrawString(value, valueFont, valueBrush,
new RectangleF(innerX, y + 14f, innerW, 14f));
}
```
**Nota Syncfusion:** `RectangleF` non supporta named arguments — usare sempre la forma posizionale `new RectangleF(x, y, w, h)`.
### Gestione casi nulli / vuoti
- **Bid e Ask**: se `string.IsNullOrEmpty(info.Bid) || string.IsNullOrEmpty(info.Ask)`, i box Bid e Ask **non vengono disegnati**; la larghezza liberata viene assorbita dal box Tipologia. Il box Data viene comunque disegnato se `LastPriceDate` non è vuoto.
- **Data**: se `string.IsNullOrEmpty(info.LastPriceDate)`, il box Data viene omesso.
- **Tipologia**: se `string.IsNullOrEmpty(info.Categoria)`, il box Tipologia viene omesso; in quel caso i box rimanenti (Data/Bid/Ask) si dispongono da sinistra normalmente.
### Troncamento testo Tipologia
Il box Tipologia usa `RectangleF` con larghezza calcolata. Syncfusion tronca automaticamente il testo che eccede la `RectangleF` — nessuna logica extra necessaria. Il font 11pt consente fino a ~45 caratteri nella larghezza tipica (~290pt). Per tipologie più lunghe il testo viene tagliato dal renderer PDF senza errori.
### Rimozione logica precedente
Il vecchio `DrawTitle` disegnava:
- Titolo a sinistra in `w * 0.65f` + Bid/Ask come stringa a destra in `w * 0.35f`
- Tipologia come riga di testo grigio `Regular` sotto il titolo
Tutta la logica di `DrawTitle` viene riscritta. Il metodo `DrawTitle` torna `float y` (la nuova y dopo il titolo + linea + box row).
---
## 2. Rinomina colonne Tabella Sottostanti (`AnagraficaSectionRenderer.DrawSottostanti`)
Modifica solo le stringhe nell'array degli header:
| Vecchio | Nuovo |
|---------|-------|
| `"Barr.K"` | `"Barriera Capitale"` |
| `"Buffer K"` | `"Buffer Capitale"` |
| `"Trig.CPN"` | `"Trigger Cedola"` |
| `"Buf.CPN"` | `"Buffer Cedola"` |
Le altre colonne (`Nome`, `Strike`, `Last`, `% Perf.`, `Trig.AC`) rimangono invariate. Le larghezze colonna possono richiedere un piccolo aggiustamento se i nuovi header non entrano — da verificare a runtime.
---
## 3. Rinomina colonna Tabella Eventi (`EventiSectionRenderer`)
| Vecchio | Nuovo |
|---------|-------|
| `"Trigger CPN"` | `"Trigger Cedola"` |
La colonna è alla posizione 4 (0-indexed), larghezza corrente 52pt. "Trigger Cedola" a font Small ha larghezza simile a "Trigger CPN" — la larghezza 52pt è sufficiente, non richiede modifica. Da verificare visivamente dopo il build.
---
## 4. Fix footer branding (`PdfTheme.DrawFooter`)
### Situazione attuale
Il codice esistente già usa `"Powered by "` (con spazio finale) come testo di prefisso, e calcola la X del link tramite `MeasureString`. Il bug visivo è che `PdfStandardFont.MeasureString` può restituire una larghezza che esclude il trailing space, causando sovrapposizione tra "by" e il link "Smart Roots".
### Fix
Aggiungere un offset fisso di **2pt** alla X del link, dopo il valore restituito da `MeasureString`:
```csharp
// PRIMA:
float prefixWidth = prefixFont.MeasureString(prefixText).Width;
webLink.DrawTextWebLink(g, new PointF(prefixWidth, footerY));
// DOPO:
float prefixWidth = prefixFont.MeasureString(prefixText).Width + 2f;
webLink.DrawTextWebLink(g, new PointF(prefixWidth, footerY));
```
---
## Files modificati
| File | Modifica |
|------|----------|
| `CertReports.Syncfusion/Helpers/PdfTheme.cs` | Nuove costanti colore/brush, fix footer offset +2pt |
| `CertReports.Syncfusion/Services/Implementations/AnagraficaSectionRenderer.cs` | Riscrittura `DrawTitle`, helper `DrawInfoBox`, rinomina colonne sottostanti |
| `CertReports.Syncfusion/Services/Implementations/EventiSectionRenderer.cs` | Rinomina colonna "Trigger CPN" → "Trigger Cedola" |
---
## Non in scope
- Modifiche al grafico, agli scenari, o ad altre sezioni del report
- Modifiche al DB o alle stored procedure
- Modifiche al layout delle sezioni A, B del report (Caratteristiche, Analisi)