# 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 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 GenerateReportAsync(string isin, bool showBranding = false); } ``` - [ ] **Step 1.3 — Aggiornare `ReportOrchestrator.GenerateReportAsync`** In `Services/Implementations/ReportOrchestrator.cs`: ```csharp public async Task 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 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 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 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 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 /// /// 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. /// 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; /// /// Sezione 1 — Prima pagina del report: Anagrafica + Analisi + Sottostanti. /// Struttura identica al vecchio report DevExpress, stile "Ibrido elegante". /// 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 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