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
ShowBrandingaCertificateReportDataIn
Models/CertificateModels.cs, nella classeCertificateReportData(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
IReportOrchestratorIn
Services/Interfaces/IServices.cs, aggiungere il parametroshowBranding:public interface IReportOrchestrator { Task<byte[]> GenerateReportAsync(string isin, bool showBranding = false); } -
Step 1.3 — Aggiornare
ReportOrchestrator.GenerateReportAsyncIn
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
reportDatae le due righe cache. Il resto del metodo rimane identico. -
Step 1.4 — Aggiornare
ReportControllerper 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 buildAtteso:
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)); }PdfTextWebLinkrichiedeusing Syncfusion.Pdf.Interactive;— aggiungere all'inizio del filePdfTheme.cs. -
Step 2.5 — Build e verifica
dotnet buildAtteso:
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 variabilestartYusata nel calcolo del bordo esterno va dichiarata prima del ciclo delle righe. Aggiungerefloat startY = y;subito dopoy += rh;(dopo l'header). -
Step 3.2 — Correggere bordo inferiore in
DrawEmittenteTableIl codice dell'Step 3.1 contiene
new RectangleF(x, startY: y - ..., ...)che non compila (RectangleFnon supporta named arguments). Rimuovere completamente quelDrawRectanglee 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 buildAtteso:
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.SyncfusionAprire nel browser:
https://localhost:{porta}/api/report/by-isin/DE000UL00754→ verifica pagina 1 con nuova strutturahttps://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
PdfTextWebLinkrichiedeusing Syncfusion.Pdf.Interactive;— aggiungere all'header diPdfTheme.cs- La cache key ora è
{isin}:brandedper 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)passandoreportData—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