Compare commits
10 Commits
8419ac0a78
...
c1d274e0b6
| Author | SHA1 | Date | |
|---|---|---|---|
| c1d274e0b6 | |||
| 4e36c69ab2 | |||
| aefba39739 | |||
| 3694ce8780 | |||
| c8f7452e2b | |||
| e76fdc1bb0 | |||
| 3636676dbc | |||
| c72670edee | |||
| eb5a8d6d7b | |||
| ff2f4ccfe6 |
109
CLAUDE.md
Normal file
109
CLAUDE.md
Normal 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`.
|
||||||
@@ -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,
|
||||||
|
|||||||
@@ -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();
|
||||||
|
|
||||||
|
|||||||
@@ -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"
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
303
docs/superpowers/plans/2026-03-20-page1-header-restyle.md
Normal file
303
docs/superpowers/plans/2026-03-20-page1-header-restyle.md
Normal 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
|
||||||
176
docs/superpowers/specs/2026-03-20-page1-header-restyle-design.md
Normal file
176
docs/superpowers/specs/2026-03-20-page1-header-restyle-design.md
Normal 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)
|
||||||
Reference in New Issue
Block a user