docs: add all superpowers plans and specs

This commit is contained in:
2026-03-21 11:54:39 +01:00
parent bd5941185d
commit 16d06340fc
2 changed files with 1027 additions and 0 deletions

View File

@@ -0,0 +1,852 @@
# Anagrafica Redesign + Footer Branding — 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:** Ridisegnare la Sezione 1 (Anagrafica) del report PDF con stile "Ibrido elegante", riordinare i campi come nel vecchio report DevExpress, portare la tabella Sottostanti in pagina 1, e aggiungere il parametro `?branding=true` per il footer Smart Roots.
**Architecture:** Quattro modifiche indipendenti in cascata: (1) propagazione del flag `ShowBranding` dal controller fino a `CertificateReportData`; (2) nuovi colori e helper `DrawFooter` in `PdfTheme`; (3) riscrittura completa di `AnagraficaSectionRenderer`; (4) aggiornamento cache key per includere il parametro branding.
**Tech Stack:** ASP.NET Core 8, Syncfusion.Pdf.Net.Core v33, C# 12. Nessun test runner — verifica manuale via `dotnet run` + browser con ISIN reale.
**Spec:** `docs/superpowers/specs/2026-03-20-anagrafica-redesign-design.md`
---
## File Map
| File | Azione | Responsabilità |
|------|--------|----------------|
| `CertReports.Syncfusion/Services/Interfaces/IServices.cs` | Modify | Aggiungere overload `GenerateReportAsync(string isin, bool showBranding)` a `IReportOrchestrator` |
| `CertReports.Syncfusion/Models/CertificateModels.cs` | Modify | Aggiungere `bool ShowBranding` a `CertificateReportData` |
| `CertReports.Syncfusion/Controllers/ReportController.cs` | Modify | Leggere `?branding=` da query string, passarlo all'orchestratore |
| `CertReports.Syncfusion/Services/Implementations/ReportOrchestrator.cs` | Modify | Firma `GenerateReportAsync` con `bool showBranding`, impostare `reportData.ShowBranding` |
| `CertReports.Syncfusion/Helpers/PdfTheme.cs` | Modify | Nuovi colori, riduzione margini/row height, metodo `DrawFooter()` |
| `CertReports.Syncfusion/Services/Implementations/AnagraficaSectionRenderer.cs` | Rewrite | Nuova struttura 3 sezioni, stile C, sottostanti in pagina 1 |
---
## Task 1: Propagazione flag `ShowBranding`
Aggiunge `ShowBranding` al modello dati e al flusso controller → orchestratore, senza ancora usarlo nel rendering.
**Files:**
- Modify: `CertReports.Syncfusion/Models/CertificateModels.cs`
- Modify: `CertReports.Syncfusion/Services/Interfaces/IServices.cs`
- Modify: `CertReports.Syncfusion/Controllers/ReportController.cs`
- Modify: `CertReports.Syncfusion/Services/Implementations/ReportOrchestrator.cs`
- [ ] **Step 1.1 — Aggiungere `ShowBranding` a `CertificateReportData`**
In `Models/CertificateModels.cs`, nella classe `CertificateReportData` (riga 152), aggiungere la proprietà:
```csharp
public class CertificateReportData
{
public CertificateInfo Info { get; set; } = new();
public List<CertificateEvent> Eventi { get; set; } = new();
public ScenarioAnalysis Scenario { get; set; } = new();
public byte[]? ChartImage { get; set; }
public bool ShowBranding { get; set; } = false; // ← aggiunto
}
```
- [ ] **Step 1.2 — Aggiornare l'interfaccia `IReportOrchestrator`**
In `Services/Interfaces/IServices.cs`, aggiungere il parametro `showBranding`:
```csharp
public interface IReportOrchestrator
{
Task<byte[]> GenerateReportAsync(string isin, bool showBranding = false);
}
```
- [ ] **Step 1.3 — Aggiornare `ReportOrchestrator.GenerateReportAsync`**
In `Services/Implementations/ReportOrchestrator.cs`:
```csharp
public async Task<byte[]> GenerateReportAsync(string isin, bool showBranding = false)
{
// ── Cache check (chiave include branding) ─────────────────────────
var cacheKey = showBranding ? $"{isin}:branded" : isin;
var cached = _cache.Get(cacheKey);
if (cached != null)
{
_logger.LogInformation("Report per ISIN {Isin} servito da cache ({Size} bytes)", isin, cached.Length);
return cached;
}
_logger.LogInformation("Inizio generazione report per ISIN {Isin}", isin);
// ── 1. Recupera tutti i dati ───────────────────────────────────
var reportData = new CertificateReportData
{
Info = await _dataService.GetCertificateInfoAsync(isin),
Eventi = await _dataService.GetCertificateEventsAsync(isin),
Scenario = await _dataService.GetScenarioAnalysisAsync(isin),
// ChartImage viene popolato più avanti dall'orchestratore — non aggiungere qui
ShowBranding = showBranding,
};
// Tutto il resto del metodo è invariato.
// Sostituire SOLO le due righe della cache:
// _cache.Get(isin) → _cache.Get(cacheKey)
// _cache.Set(isin, ...) → _cache.Set(cacheKey, finalPdf)
}
```
> Nota: modificare solo la firma, l'inizializzazione di `reportData` e le due righe cache. Il resto del metodo rimane identico.
- [ ] **Step 1.4 — Aggiornare `ReportController` per leggere `?branding=`**
In `Controllers/ReportController.cs`, aggiornare tutti e tre i punti dove si chiama `_orchestrator.GenerateReportAsync`:
**Endpoint `GenerateReport` (GET /api/report):**
```csharp
[HttpGet]
public async Task<IActionResult> GenerateReport(
[FromQuery(Name = "p")] string? encryptedIsin = null,
[FromQuery(Name = "alias")] string? aliasId = null,
[FromQuery(Name = "branding")] bool showBranding = false) // ← aggiunto
{
// ... logica risoluzione ISIN invariata ...
return await GenerateAndReturnPdf(isin, showBranding);
}
```
**Endpoint `GenerateReportByIsin` (GET /api/report/by-isin/{isin}):**
```csharp
[HttpGet("by-isin/{isin}")]
public async Task<IActionResult> GenerateReportByIsin(
string isin,
[FromQuery(Name = "branding")] bool showBranding = false) // ← aggiunto
{
if (string.IsNullOrWhiteSpace(isin) || isin.Length < 12)
return BadRequest("ISIN non valido.");
return await GenerateAndReturnPdf(isin, showBranding);
}
```
**Endpoint `DownloadReport` (GET /api/report/download):**
```csharp
[HttpGet("download")]
public async Task<IActionResult> DownloadReport(
[FromQuery(Name = "p")] string? encryptedIsin = null,
[FromQuery(Name = "alias")] string? aliasId = null,
[FromQuery(Name = "branding")] bool showBranding = false) // ← aggiunto
{
// ... logica risoluzione ISIN invariata ...
var pdfBytes = await _orchestrator.GenerateReportAsync(isin, showBranding);
return File(pdfBytes, "application/pdf", $"{isin}.pdf");
}
```
**Helper privato `GenerateAndReturnPdf`:**
```csharp
private async Task<IActionResult> GenerateAndReturnPdf(string isin, bool showBranding)
{
try
{
_logger.LogInformation("Richiesta report per ISIN {Isin}", isin);
var pdfBytes = await _orchestrator.GenerateReportAsync(isin, showBranding);
Response.Headers.Append("Content-Disposition", $"inline; filename={isin}.pdf");
return File(pdfBytes, "application/pdf");
}
catch (Exception ex)
{
_logger.LogError(ex, "Errore generazione report per ISIN {Isin}", isin);
return StatusCode(500, "Errore nella generazione del report.");
}
}
```
- [ ] **Step 1.5 — Build e verifica compilazione**
```bash
cd "CertReports.Syncfusion"
dotnet build
```
Atteso: `Build succeeded. 0 Error(s)`. Nessun warning su interfacce non implementate.
- [ ] **Step 1.6 — Commit**
```bash
git add CertReports.Syncfusion/Models/CertificateModels.cs \
CertReports.Syncfusion/Services/Interfaces/IServices.cs \
CertReports.Syncfusion/Controllers/ReportController.cs \
CertReports.Syncfusion/Services/Implementations/ReportOrchestrator.cs
git commit -m "feat: add ShowBranding flag propagation from API to report data"
```
---
## Task 2: Aggiornare `PdfTheme` — nuovi colori, margini ridotti, `DrawFooter()`
**Files:**
- Modify: `CertReports.Syncfusion/Helpers/PdfTheme.cs`
- [ ] **Step 2.1 — Aggiungere nuovi colori e costanti**
Aggiungere in coda al blocco `// ─── Colori ───` (dopo riga 24):
```csharp
// Nuovi colori stile "Ibrido elegante"
public static readonly Color AccentBlue = Color.FromArgb(255, 21, 101, 192); // #1565C0
public static readonly Color NegativeRed = Color.FromArgb(255, 204, 0, 0); // #CC0000
public static readonly Color PositiveGreen = Color.FromArgb(255, 46, 125, 50); // #2E7D32
public static readonly Color FooterText = Color.FromArgb(255, 102, 102, 102); // #666666
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 TableAltRow = Color.FromArgb(255, 247, 249, 252); // #F7F9FC
public static readonly Color TableHeaderBg = Color.FromArgb(255, 21, 101, 192); // #1565C0 (same as AccentBlue)
```
Aggiungere i brush/pen corrispondenti in coda al blocco `// ─── Brushes ───`:
```csharp
public static PdfBrush AccentBlueBrush => new PdfSolidBrush(AccentBlue);
public static PdfBrush NegativeBrush2 => new PdfSolidBrush(NegativeRed);
public static PdfBrush PositiveBrush2 => new PdfSolidBrush(PositiveGreen);
public static PdfBrush FooterTextBrush => new PdfSolidBrush(FooterText);
public static PdfBrush TableAltRowBrush => new PdfSolidBrush(TableAltRow);
public static PdfBrush TableHeaderBrush => new PdfSolidBrush(TableHeaderBg);
public static PdfPen SeparatorPen => new PdfPen(SeparatorLine, 0.5f);
public static PdfPen TableBorderPen => new PdfPen(TableBorder, 0.5f);
public static PdfPen AccentBluePen => new PdfPen(AccentBlue, 2.5f);
```
- [ ] **Step 2.2 — Ridurre margini e row height**
Modificare le costanti di layout esistenti (righe 43-46):
```csharp
// ─── Margini & Layout ──────────────────────────────────────────────
public const float PageMargin = 36f; // era 40f
public const float RowHeight = 14f; // era 18f — KV pairs sezione Analisi
public const float HeaderRowHeight = 18f; // era 22f — header sezioni
public const float CellPadding = 4f;
public const float FooterHeight = 28f; // spazio riservato footer in fondo alla pagina
```
- [ ] **Step 2.3 — Aggiungere font per footer e tabella sottostanti**
In coda al blocco `// ─── Font ───`:
```csharp
private static readonly PdfStandardFont _fontFooter = new(PdfFontFamily.Helvetica, 7f);
private static readonly PdfStandardFont _fontFooterBold = new(PdfFontFamily.Helvetica, 7f, PdfFontStyle.Bold);
private static readonly PdfStandardFont _fontTable = new(PdfFontFamily.Helvetica, 7.5f);
private static readonly PdfStandardFont _fontTableBold = new(PdfFontFamily.Helvetica, 7.5f, PdfFontStyle.Bold);
public static PdfFont Footer => _fontFooter;
public static PdfFont FooterBold => _fontFooterBold;
public static PdfFont TableFont => _fontTable;
public static PdfFont TableBold => _fontTableBold;
```
- [ ] **Step 2.4 — Aggiungere helper `DrawFooter()`**
Aggiungere alla fine della classe `PdfTheme`, prima della chiusura `}`:
```csharp
/// <summary>
/// Disegna il footer in fondo alla pagina.
/// Se showBranding=true: "Powered by [Smart Roots↗]" a sx + numero pagina a dx.
/// Se showBranding=false: solo numero pagina centrato.
/// </summary>
public static void DrawFooter(
PdfGraphics g,
float pageWidth,
float pageHeight,
int pageNumber,
bool showBranding)
{
float footerY = pageHeight - FooterHeight + 6f;
// Linea separatrice
g.DrawLine(SeparatorPen, 0, footerY - 4f, pageWidth, footerY - 4f);
string pageLabel = pageNumber.ToString();
if (!showBranding)
{
// Solo numero pagina centrato
g.DrawString(pageLabel, Footer, FooterTextBrush,
new RectangleF(0, footerY, pageWidth, 12f),
new PdfStringFormat(PdfTextAlignment.Center));
return;
}
// "Powered by " testo normale
const string prefix = "Powered by ";
const string linkText = "Smart Roots";
const string linkUrl = "https://www.smart-roots.net";
float prefixWidth = Footer.MeasureString(prefix).Width;
float linkWidth = Footer.MeasureString(linkText).Width;
g.DrawString(prefix, Footer, FooterTextBrush,
new RectangleF(0, footerY, prefixWidth + 2f, 12f));
// "Smart Roots" come hyperlink PDF
var webLink = new PdfTextWebLink
{
Url = linkUrl,
Text = linkText,
Font = Footer,
Brush = AccentBlueBrush,
};
webLink.DrawTextWebLink(g, new PointF(prefixWidth, footerY));
// Numero pagina a destra
g.DrawString(pageLabel, Footer, FooterTextBrush,
new RectangleF(0, footerY, pageWidth, 12f),
new PdfStringFormat(PdfTextAlignment.Right));
}
```
> `PdfTextWebLink` richiede `using Syncfusion.Pdf.Interactive;` — aggiungere all'inizio del file `PdfTheme.cs`.
- [ ] **Step 2.5 — Build e verifica**
```bash
dotnet build
```
Atteso: `Build succeeded. 0 Error(s)`.
- [ ] **Step 2.6 — Commit**
```bash
git add CertReports.Syncfusion/Helpers/PdfTheme.cs
git commit -m "feat: add AccentBlue palette, reduced margins, DrawFooter with branding support"
```
---
## Task 3: Riscrivere `AnagraficaSectionRenderer`
Sostituzione completa del metodo `Render()` e degli helper privati. La struttura passa da 4 blocchi KV orizzontali a 3 sezioni verticali: Caratteristiche Prodotto, Analisi, Sottostanti.
**Files:**
- Rewrite: `CertReports.Syncfusion/Services/Implementations/AnagraficaSectionRenderer.cs`
- [ ] **Step 3.1 — Sostituire la classe completa**
Sostituire l'intero contenuto del file con:
```csharp
using CertReports.Syncfusion.Helpers;
using CertReports.Syncfusion.Models;
using CertReports.Syncfusion.Services.Interfaces;
using Syncfusion.Drawing;
using Syncfusion.Pdf;
using Syncfusion.Pdf.Graphics;
using Syncfusion.Pdf.Grid;
namespace CertReports.Syncfusion.Services.Implementations;
/// <summary>
/// Sezione 1 — Prima pagina del report: Anagrafica + Analisi + Sottostanti.
/// Struttura identica al vecchio report DevExpress, stile "Ibrido elegante".
/// </summary>
public class AnagraficaSectionRenderer : IPdfSectionRenderer
{
public string SectionName => "Anagrafica";
public int Order => 1;
// Larghezza area utile (A4 595pt - 2*36 margini)
private const float PageW = 595f - 2 * PdfTheme.PageMargin;
// Altezza area utile (A4 842pt - 2*36 margini - footer)
private const float PageH = 842f - 2 * PdfTheme.PageMargin - PdfTheme.FooterHeight;
private const float ColGap = 10f;
private const float ColW = (PageW - ColGap) / 2f;
private const float SectionGap = 10f;
public PdfDocument Render(CertificateReportData data)
{
var doc = new PdfDocument();
var page = PdfTheme.AddA4Page(doc);
var g = page.Graphics;
var info = data.Info;
float y = 0f;
// ── TITOLO ────────────────────────────────────────────────────
y = DrawTitle(g, info, PageW, y);
// ── SEZIONE A: CARATTERISTICHE PRODOTTO ───────────────────────
y = DrawSectionLabel(g, "Caratteristiche Prodotto", y);
y = DrawCaratteristiche(g, info, y);
y += SectionGap;
// ── SEZIONE B: ANALISI ────────────────────────────────────────
y = DrawSectionLabel(g, "Analisi", y);
y = DrawAnalisi(g, info, y);
y += SectionGap;
// ── SEZIONE C: SOTTOSTANTI ────────────────────────────────────
if (info.Sottostanti.Count > 0)
{
// Se lo spazio rimanente è meno di 80pt, nuova pagina
if (y > PageH - 80f)
{
PdfTheme.DrawFooter(g, PageW, PageH, 1, data.ShowBranding);
page = doc.Pages.Add();
g = page.Graphics;
y = 0f;
}
y = DrawSectionLabel(g, "Sottostanti", y);
DrawSottostanti(g, info.Sottostanti, PageW, y);
}
PdfTheme.DrawFooter(g, PageW, PageH, doc.Pages.Count, data.ShowBranding);
return doc;
}
// ═══════════════════════════════════════════════════════════════
// TITOLO con Bid/Ask a destra
// ═══════════════════════════════════════════════════════════════
private float DrawTitle(PdfGraphics g, CertificateInfo info, float w, float y)
{
// ISIN + nome prodotto a sinistra
g.DrawString($"Scheda Prodotto {info.Isin}",
PdfTheme.SectionTitleFont,
new PdfSolidBrush(PdfTheme.AccentBlue),
new RectangleF(0, y, w * 0.65f, 20f));
// Bid/Ask a destra
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
g.DrawLine(PdfTheme.AccentBluePen, 0, y + 2f, w, y + 2f);
y += 8f;
return y;
}
// ═══════════════════════════════════════════════════════════════
// INTESTAZIONE DI SEZIONE (accent line laterale + testo blu)
// ═══════════════════════════════════════════════════════════════
private float DrawSectionLabel(PdfGraphics g, string title, float y)
{
// Accent line verticale sinistra
g.DrawRectangle(PdfTheme.AccentBlueBrush, new RectangleF(0, y, 3f, 14f));
g.DrawString(title, PdfTheme.Bold,
new PdfSolidBrush(PdfTheme.AccentBlue),
new RectangleF(6f, y, PageW, 14f));
return y + 16f;
}
// ═══════════════════════════════════════════════════════════════
// SEZIONE A: CARATTERISTICHE PRODOTTO
// Sinistra: tabella emittente | Destra: cedole + rendimento totale
// ═══════════════════════════════════════════════════════════════
private float DrawCaratteristiche(PdfGraphics g, CertificateInfo info, float y)
{
float startY = y;
// ── Colonna sinistra: tabella emittente ──
float leftY = DrawEmittenteTable(g, info, 0, ColW, y);
// ── Colonna destra: cedole + rendimento totale ──
float rightX = ColW + ColGap;
float rightY = DrawCedoleTable(g, info, rightX, ColW, y);
return Math.Max(leftY, rightY);
}
private float DrawEmittenteTable(PdfGraphics g, CertificateInfo info, float x, float w, float y)
{
float rh = PdfTheme.RowHeight;
float pad = PdfTheme.CellPadding;
// Header blu
g.DrawRectangle(PdfTheme.TableHeaderBrush, new RectangleF(x, y, w, rh));
string headerText = string.IsNullOrEmpty(info.Emittente)
? "EMITTENTE"
: $"EMITTENTE {info.Emittente.ToUpper()}";
g.DrawString(headerText, PdfTheme.TableBold,
PdfTheme.HeaderTextBrush,
new RectangleF(x + pad, y + 3f, w - pad * 2, rh));
y += rh;
// Righe dati
var rows = new[]
{
("ISIN", info.Isin),
("Mercato", info.Mercato),
("Valuta", info.Valuta),
("Data Emissione", info.DataEmissione),
("Data Scadenza", info.Scadenza),
("Prossimo Autocall", info.NextAutocallDate),
};
for (int i = 0; i < rows.Length; i++)
{
var (label, value) = rows[i];
if (string.IsNullOrWhiteSpace(value)) continue;
bool alt = i % 2 == 1;
if (alt)
g.DrawRectangle(new PdfSolidBrush(PdfTheme.TableAltRow),
new RectangleF(x, y, w, rh));
g.DrawLine(PdfTheme.TableBorderPen, x, y, x + w, y);
g.DrawString(label, PdfTheme.TableFont,
new PdfSolidBrush(PdfTheme.TextSecondary),
new RectangleF(x + pad, y + 2f, w * 0.5f, rh));
var valueBrush = (label == "ISIN" || label == "Prossimo Autocall")
? new PdfSolidBrush(PdfTheme.AccentBlue)
: new PdfSolidBrush(PdfTheme.TextPrimary);
g.DrawString(value, PdfTheme.TableBold, valueBrush,
new RectangleF(x + w * 0.5f, y + 2f, w * 0.5f - pad, rh),
new PdfStringFormat(PdfTextAlignment.Right));
y += rh;
}
// Bordo esterno tabella
g.DrawRectangle(PdfTheme.TableBorderPen,
new RectangleF(x, startY: y - rows.Length * rh - rh, w, rows.Length * rh + rh));
return y + 2f;
}
private float DrawCedoleTable(PdfGraphics g, CertificateInfo info, float x, float w, float y)
{
float rh = PdfTheme.RowHeight;
float pad = PdfTheme.CellPadding;
// Header blu
g.DrawRectangle(PdfTheme.TableHeaderBrush, new RectangleF(x, y, w, rh));
g.DrawString("CEDOLE E RENDIMENTO", PdfTheme.TableBold,
PdfTheme.HeaderTextBrush,
new RectangleF(x + pad, y + 3f, w - pad * 2, rh));
y += rh;
var rows = new (string Label, string Value, bool IsNegative)[]
{
("Importo Cedole Pagate", info.CpnPagati?.ToString("N2") ?? "-", false),
("Importo Cedole da Pagare", info.CpnDaPagare?.ToString("N2") ?? "-", false),
("Importo Cedole in Memoria",info.CpnInMemoria?.ToString("N2") ?? "-", false),
("Rendimento Totale", info.RendimentoAttuale, IsNegativeValue(info.RendimentoAttuale)),
};
for (int i = 0; i < rows.Length; i++)
{
var (label, value, isNeg) = rows[i];
bool alt = i % 2 == 1;
if (alt)
g.DrawRectangle(new PdfSolidBrush(PdfTheme.TableAltRow),
new RectangleF(x, y, w, rh));
g.DrawLine(PdfTheme.TableBorderPen, x, y, x + w, y);
g.DrawString(label, PdfTheme.TableFont,
new PdfSolidBrush(PdfTheme.TextSecondary),
new RectangleF(x + pad, y + 2f, w * 0.65f, rh));
var valueBrush = isNeg
? new PdfSolidBrush(PdfTheme.NegativeRed)
: new PdfSolidBrush(PdfTheme.TextPrimary);
var valueFont = label == "Rendimento Totale" ? PdfTheme.TableBold : PdfTheme.TableFont;
g.DrawString(value, valueFont, valueBrush,
new RectangleF(x + w * 0.65f, y + 2f, w * 0.35f - pad, rh),
new PdfStringFormat(PdfTextAlignment.Right));
y += rh;
}
g.DrawLine(PdfTheme.TableBorderPen, x, y, x + w, y);
return y + 2f;
}
// ═══════════════════════════════════════════════════════════════
// SEZIONE B: ANALISI
// Sinistra: caratteristiche prodotto | Destra: rendimenti
// ═══════════════════════════════════════════════════════════════
private float DrawAnalisi(PdfGraphics g, CertificateInfo info, float y)
{
var leftItems = new (string Label, string Value)[]
{
("Importo Cedola (p.a.)", info.NominalAnnualYield),
("Frequenza Cedola", info.FrequenzaCedole),
("Valore Nominale", info.NominalValue?.ToString("N0") ?? "-"),
("Prezzo Emissione", info.PrezzoEmissione?.ToString("N0") ?? "-"),
("Barriera Capitale", info.LivelloBarriera),
("Tipo Barriera", info.BarrierType),
("Tipo Basket", info.BasketType),
("Leva", string.IsNullOrWhiteSpace(info.Leva) ? "—" : info.Leva),
};
var rightItems = new (string Label, string Value)[]
{
("Rend. Capitale a Scadenza", info.CapitalReturnAtMaturity),
("IRR", info.IRR),
("Protezione Capitale", info.BufferKProt),
("Protezione Coupon", info.BufferCPNProt),
("Valore Autocall", info.AutocallValue),
("Distanza Autocall", info.TriggerAutocallDistance),
("Rendimento Autocall", info.AutocallReturn),
("Fattore Airbag", string.IsNullOrWhiteSpace(info.FattoreAirbag) ? "—" : info.FattoreAirbag),
("Trigger OneStar", string.IsNullOrWhiteSpace(info.TriggerOneStar) ? "—" : info.TriggerOneStar),
};
float leftY = DrawKVList(g, leftItems, 0, ColW, y);
float rightY = DrawKVList(g, rightItems, ColW + ColGap, ColW, y);
return Math.Max(leftY, rightY);
}
private float DrawKVList(PdfGraphics g, (string Label, string Value)[] items, float x, float w, float y)
{
float rh = PdfTheme.RowHeight;
float pad = PdfTheme.CellPadding;
float labelW = w * 0.58f;
float valueW = w * 0.42f;
foreach (var (label, value) in items)
{
if (string.IsNullOrWhiteSpace(value)) continue;
g.DrawString(label, PdfTheme.TableFont,
new PdfSolidBrush(PdfTheme.TextSecondary),
new RectangleF(x + pad, y + 1f, labelW, rh));
bool isNeg = IsNegativeValue(value);
bool isKey = label is "Importo Cedola (p.a.)" or "Barriera Capitale";
var brush = isNeg ? new PdfSolidBrush(PdfTheme.NegativeRed)
: isKey ? new PdfSolidBrush(PdfTheme.AccentBlue)
: new PdfSolidBrush(PdfTheme.TextPrimary);
g.DrawString(value, PdfTheme.TableBold, brush,
new RectangleF(x + labelW, y + 1f, valueW - pad, rh),
new PdfStringFormat(PdfTextAlignment.Right));
g.DrawLine(new PdfPen(PdfTheme.SeparatorLine, 0.3f),
x, y + rh, x + w, y + rh);
y += rh;
}
return y;
}
// ═══════════════════════════════════════════════════════════════
// SEZIONE C: SOTTOSTANTI (9 colonne, font 7.5pt)
// ═══════════════════════════════════════════════════════════════
private void DrawSottostanti(PdfGraphics g, List<Sottostante> sottostanti, float w, float y)
{
var grid = new PdfGrid();
grid.Style.CellPadding = new PdfPaddings(2, 2, 2, 2);
// 9 colonne (Dist.AC rimossa rispetto alla versione precedente)
string[] headers = { "Nome", "Strike", "Last", "% Perf.", "Barr.K",
"Buffer K", "Trig.CPN", "Buf.CPN", "Trig.AC" };
foreach (var _ in headers) grid.Columns.Add();
// Header
var hr = grid.Headers.Add(1)[0];
for (int i = 0; i < headers.Length; i++)
{
hr.Cells[i].Value = headers[i];
hr.Cells[i].Style.Font = PdfTheme.TableBold;
hr.Cells[i].Style.BackgroundBrush = PdfTheme.TableHeaderBrush;
hr.Cells[i].Style.TextBrush = PdfTheme.HeaderTextBrush as PdfBrush;
hr.Cells[i].StringFormat = new PdfStringFormat(
i == 0 ? PdfTextAlignment.Left : PdfTextAlignment.Center);
}
// Righe dati
for (int i = 0; i < sottostanti.Count; i++)
{
var s = sottostanti[i];
var row = grid.Rows.Add();
row.Cells[0].Value = s.Nome;
row.Cells[1].Value = s.Strike;
row.Cells[2].Value = s.LastPrice;
row.Cells[3].Value = s.Performance;
row.Cells[4].Value = s.CapitalBarrier;
row.Cells[5].Value = s.ULCapitalBarrierBuffer;
row.Cells[6].Value = s.CouponBarrier;
row.Cells[7].Value = s.ULCouponBarrierBuffer;
row.Cells[8].Value = s.TriggerAutocall;
for (int c = 0; c < headers.Length; c++)
{
row.Cells[c].Style.Font = PdfTheme.TableFont;
row.Cells[c].StringFormat = new PdfStringFormat(
c == 0 ? PdfTextAlignment.Left : PdfTextAlignment.Right);
}
// Colori performance e buffer negativi
ColorPerformanceCell(row.Cells[3], s.Performance); // % Perf.
ColorPerformanceCell(row.Cells[5], s.ULCapitalBarrierBuffer); // Buffer K
ColorPerformanceCell(row.Cells[7], s.ULCouponBarrierBuffer); // Buf.CPN
// Righe alternate
if (i % 2 == 1)
for (int c = 0; c < headers.Length; c++)
row.Cells[c].Style.BackgroundBrush = PdfTheme.TableAltRowBrush;
}
// Larghezze proporzionali (totale = PageW)
float[] cw = { 90f, 52f, 52f, 46f, 52f, 46f, 52f, 46f, 46f };
float total = cw.Sum();
float scale = w / total;
for (int i = 0; i < cw.Length; i++)
grid.Columns[i].Width = cw[i] * scale;
PdfTheme.ApplyThinBorders(grid);
grid.Draw(g, new PointF(0, y));
}
// ═══════════════════════════════════════════════════════════════
// Helper
// ═══════════════════════════════════════════════════════════════
private static bool IsNegativeValue(string value)
{
if (string.IsNullOrWhiteSpace(value)) return false;
// Considera negativo se inizia con '-' dopo trim di spazi e simboli
var trimmed = value.TrimStart();
return trimmed.StartsWith('-');
}
private static void ColorPerformanceCell(PdfGridCell cell, string value)
{
if (IsNegativeValue(value))
cell.Style.TextBrush = new PdfSolidBrush(PdfTheme.NegativeRed);
else if (!string.IsNullOrWhiteSpace(value) && value != "-" && value != "—")
cell.Style.TextBrush = new PdfSolidBrush(PdfTheme.PositiveGreen);
}
}
```
> **Nota su `DrawEmittenteTable`:** La variabile `startY` usata nel calcolo del bordo esterno va dichiarata prima del ciclo delle righe. Aggiungere `float startY = y;` subito dopo `y += rh;` (dopo l'header).
- [ ] **Step 3.2 — Correggere bordo inferiore in `DrawEmittenteTable`**
Il codice dell'Step 3.1 contiene `new RectangleF(x, startY: y - ..., ...)` che non compila (`RectangleF` non supporta named arguments). Rimuovere completamente quel `DrawRectangle` e aggiungere invece una sola linea orizzontale finale sotto l'ultima riga:
```csharp
// Sostituire il blocco DrawRectangle bordo esterno con questa singola riga:
g.DrawLine(PdfTheme.TableBorderPen, x, y, x + w, y);
```
Le linee orizzontali su ogni riga + l'header blu rendono il bordo esterno superfluo.
- [ ] **Step 3.3 — Build e verifica**
```bash
dotnet build
```
Atteso: `Build succeeded. 0 Error(s)`. Se ci sono errori di compilazione, correggerli prima di procedere.
- [ ] **Step 3.4 — Avviare l'applicazione e testare visivamente**
```bash
dotnet run --project CertReports.Syncfusion
```
Aprire nel browser:
- `https://localhost:{porta}/api/report/by-isin/DE000UL00754` → verifica pagina 1 con nuova struttura
- `https://localhost:{porta}/api/report/by-isin/DE000UL00754?branding=true` → verifica footer con link Smart Roots
**Checklist visiva pagina 1:**
- [ ] Titolo "Scheda Prodotto DE000UL00754" in blu, Bid/Ask a destra
- [ ] Linea separatrice blu sotto il titolo
- [ ] Sezione "Caratteristiche Prodotto" — tabella emittente a sinistra con header blu
- [ ] Cedole pagate/da pagare/in memoria + Rendimento Totale a destra (rosso se negativo)
- [ ] Sezione "Analisi" — 8 campi a sinistra, 9 a destra con KV pairs
- [ ] Leva, Fattore Airbag, Trigger OneStar mostrati come "—"
- [ ] Sezione "Sottostanti" — 9 colonne, valori negativi in rosso, tutto in pagina 1
- [ ] Footer: solo "1" centrato senza branding
- [ ] Footer con `?branding=true`: "Powered by Smart Roots" (link blu) + "1" a destra
- [ ] **Step 3.5 — Commit finale**
```bash
git add CertReports.Syncfusion/Services/Implementations/AnagraficaSectionRenderer.cs
git commit -m "feat: redesign page 1 - hybrid style, 3-section layout, sottostanti on page 1"
```
---
## Task 4: Verifica regressione sezioni 2-4
Controllare che le sezioni successive (Eventi, Scenario, Grafico) non siano state impattate dalla riduzione dei margini in `PdfTheme`.
**Files:** nessuno da modificare — solo verifica.
- [ ] **Step 4.1 — Verificare le sezioni successive**
Aprire nel browser il report completo e scorrere tutte le pagine:
- **Pagina 2 (Lista Eventi):** le colonne della tabella devono essere leggibili e non tagliate
- **Pagina 3+ (Analisi Scenario):** gradiente rosso→verde intatto, valori negativi in rosso
- **Ultima pagina (Grafico):** linee, legenda e assi corretti
Se la tabella Eventi o Scenario risulta più larga del previsto (per via del PageMargin ridotto da 40 a 36), aggiornare le larghezze colonne nei rispettivi renderer per sfruttare i 8pt extra disponibili.
- [ ] **Step 4.2 — Commit di eventuali aggiustamenti**
Se sono stati necessari aggiustamenti ai renderer esistenti:
```bash
# Aggiungere solo i renderer effettivamente modificati (non AnagraficaSectionRenderer)
# Esempi:
git add CertReports.Syncfusion/Services/Implementations/EventiSectionRenderer.cs
git add CertReports.Syncfusion/Services/Implementations/ScenarioSectionRenderer.cs
git commit -m "fix: adjust section renderers for updated PageMargin (36pt)"
```
---
## Note implementative
- **`PdfTextWebLink`** richiede `using Syncfusion.Pdf.Interactive;` — aggiungere all'header di `PdfTheme.cs`
- La cache key ora è `{isin}:branded` per le richieste con branding, `{isin}` per le normali — le due versioni sono cacheate separatamente
- I renderer Scenario, Eventi e Grafico chiamano già `renderer.Render(reportData)` passando `reportData` — `ShowBranding` è disponibile nel dato ma queste sezioni non lo usano (per ora non hanno footer)
- Se in futuro si vuole aggiungere il footer anche alle altre sezioni, basta aggiungere `PdfTheme.DrawFooter(...)` alla fine di ogni renderer