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