Files
SmartReports/docs/superpowers/plans/2026-03-20-anagrafica-redesign.md

37 KiB

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à:

    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:

    public interface IReportOrchestrator
    {
        Task<byte[]> GenerateReportAsync(string isin, bool showBranding = false);
    }
    
  • Step 1.3 — Aggiornare ReportOrchestrator.GenerateReportAsync

    In Services/Implementations/ReportOrchestrator.cs:

    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):

    [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}):

    [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):

    [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:

    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

    cd "CertReports.Syncfusion"
    dotnet build
    

    Atteso: Build succeeded. 0 Error(s). Nessun warning su interfacce non implementate.

  • Step 1.6 — Commit

    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):

    // 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 ───:

    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):

    // ─── 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 ───:

    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 }:

    /// <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

    dotnet build
    

    Atteso: Build succeeded. 0 Error(s).

  • Step 2.6 — Commit

    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:

    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:

    // 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

    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

    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

    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:

    # 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 reportDataShowBranding è 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