Compare commits
23 Commits
49014c1544
...
b3cf03e3d4
| Author | SHA1 | Date | |
|---|---|---|---|
| b3cf03e3d4 | |||
| 088a95cafb | |||
| f95242df1a | |||
| 1f67285ae6 | |||
| b93a16ce6e | |||
| b1003bc3f1 | |||
| 031a1acc1a | |||
| de7148c5cb | |||
| bd51bb4d26 | |||
| 15a4034a77 | |||
| 9c78b3f852 | |||
| 3e52f807a5 | |||
| 1edfccfc62 | |||
| 26f818486c | |||
| e994352bd7 | |||
| 069e33f0ec | |||
| 5b6400903d | |||
| 35b2e9ae43 | |||
| 02ca8bc9fb | |||
| f79423219f | |||
| 2ddd5af043 | |||
| cc3d3695fe | |||
| 001fbd90d9 |
@@ -43,7 +43,8 @@ public class ReportController : ControllerBase
|
|||||||
public async Task<IActionResult> GenerateReport(
|
public async Task<IActionResult> GenerateReport(
|
||||||
[FromQuery(Name = "p")] string? encryptedIsin = null,
|
[FromQuery(Name = "p")] string? encryptedIsin = null,
|
||||||
[FromQuery(Name = "alias")] string? aliasId = null,
|
[FromQuery(Name = "alias")] string? aliasId = null,
|
||||||
[FromQuery(Name = "branding")] bool showBranding = false)
|
[FromQuery(Name = "branding")] bool showBranding = false,
|
||||||
|
[FromQuery(Name = "dividend")] bool showDividend = false)
|
||||||
{
|
{
|
||||||
string? isin = null;
|
string? isin = null;
|
||||||
|
|
||||||
@@ -70,7 +71,7 @@ public class ReportController : ControllerBase
|
|||||||
return BadRequest("Specificare il parametro 'p' (ISIN cifrato) o 'alias' (alias ID).");
|
return BadRequest("Specificare il parametro 'p' (ISIN cifrato) o 'alias' (alias ID).");
|
||||||
}
|
}
|
||||||
|
|
||||||
return await GenerateAndReturnPdf(isin, showBranding);
|
return await GenerateAndReturnPdf(isin, showBranding, showDividend);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -80,14 +81,15 @@ public class ReportController : ControllerBase
|
|||||||
[HttpGet("by-isin/{isin}")]
|
[HttpGet("by-isin/{isin}")]
|
||||||
public async Task<IActionResult> GenerateReportByIsin(
|
public async Task<IActionResult> GenerateReportByIsin(
|
||||||
string isin,
|
string isin,
|
||||||
[FromQuery(Name = "branding")] bool showBranding = false)
|
[FromQuery(Name = "branding")] bool showBranding = false,
|
||||||
|
[FromQuery(Name = "dividend")] bool showDividend = false)
|
||||||
{
|
{
|
||||||
if (string.IsNullOrWhiteSpace(isin) || isin.Length < 12)
|
if (string.IsNullOrWhiteSpace(isin) || isin.Length < 12)
|
||||||
{
|
{
|
||||||
return BadRequest("ISIN non valido.");
|
return BadRequest("ISIN non valido.");
|
||||||
}
|
}
|
||||||
|
|
||||||
return await GenerateAndReturnPdf(isin, showBranding);
|
return await GenerateAndReturnPdf(isin, showBranding, showDividend);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -97,7 +99,8 @@ public class ReportController : ControllerBase
|
|||||||
public async Task<IActionResult> DownloadReport(
|
public async Task<IActionResult> DownloadReport(
|
||||||
[FromQuery(Name = "p")] string? encryptedIsin = null,
|
[FromQuery(Name = "p")] string? encryptedIsin = null,
|
||||||
[FromQuery(Name = "alias")] string? aliasId = null,
|
[FromQuery(Name = "alias")] string? aliasId = null,
|
||||||
[FromQuery(Name = "branding")] bool showBranding = false)
|
[FromQuery(Name = "branding")] bool showBranding = false,
|
||||||
|
[FromQuery(Name = "dividend")] bool showDividend = false)
|
||||||
{
|
{
|
||||||
string? isin = null;
|
string? isin = null;
|
||||||
|
|
||||||
@@ -111,7 +114,7 @@ public class ReportController : ControllerBase
|
|||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
var pdfBytes = await _orchestrator.GenerateReportAsync(isin, showBranding);
|
var pdfBytes = await _orchestrator.GenerateReportAsync(isin, showBranding, showDividend);
|
||||||
return File(pdfBytes, "application/pdf", $"{isin}.pdf");
|
return File(pdfBytes, "application/pdf", $"{isin}.pdf");
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
@@ -123,12 +126,12 @@ public class ReportController : ControllerBase
|
|||||||
|
|
||||||
// ─── Helper ────────────────────────────────────────────────────────
|
// ─── Helper ────────────────────────────────────────────────────────
|
||||||
|
|
||||||
private async Task<IActionResult> GenerateAndReturnPdf(string isin, bool showBranding)
|
private async Task<IActionResult> GenerateAndReturnPdf(string isin, bool showBranding = false, bool showDividend = false)
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
_logger.LogInformation("Richiesta report per ISIN {Isin}", isin);
|
_logger.LogInformation("Richiesta report per ISIN {Isin}", isin);
|
||||||
var pdfBytes = await _orchestrator.GenerateReportAsync(isin, showBranding);
|
var pdfBytes = await _orchestrator.GenerateReportAsync(isin, showBranding, showDividend);
|
||||||
|
|
||||||
// Inline: il PDF si apre direttamente nel browser
|
// Inline: il PDF si apre direttamente nel browser
|
||||||
Response.Headers.Append("Content-Disposition", $"inline; filename={isin}.pdf");
|
Response.Headers.Append("Content-Disposition", $"inline; filename={isin}.pdf");
|
||||||
|
|||||||
@@ -160,4 +160,5 @@ public class CertificateReportData
|
|||||||
public ScenarioAnalysis Scenario { get; set; } = new();
|
public ScenarioAnalysis Scenario { get; set; } = new();
|
||||||
public byte[]? ChartImage { get; set; }
|
public byte[]? ChartImage { get; set; }
|
||||||
public bool ShowBranding { get; set; } = false;
|
public bool ShowBranding { get; set; } = false;
|
||||||
|
public bool ShowDividend { get; set; } = false;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -45,6 +45,7 @@ builder.Services.AddScoped<IPdfSectionRenderer, AnagraficaSectionRenderer>();
|
|||||||
builder.Services.AddScoped<IPdfSectionRenderer, EventiSectionRenderer>();
|
builder.Services.AddScoped<IPdfSectionRenderer, EventiSectionRenderer>();
|
||||||
builder.Services.AddScoped<IPdfSectionRenderer, ScenarioSectionRenderer>();
|
builder.Services.AddScoped<IPdfSectionRenderer, ScenarioSectionRenderer>();
|
||||||
builder.Services.AddScoped<ExpiredAnagraficaSectionRenderer>();
|
builder.Services.AddScoped<ExpiredAnagraficaSectionRenderer>();
|
||||||
|
builder.Services.AddScoped<DividendSectionRenderer>();
|
||||||
builder.Services.AddScoped<IChartSectionRenderer, ChartSectionRenderer>();
|
builder.Services.AddScoped<IChartSectionRenderer, ChartSectionRenderer>();
|
||||||
builder.Services.AddScoped<IPdfMergerService, PdfMergerService>();
|
builder.Services.AddScoped<IPdfMergerService, PdfMergerService>();
|
||||||
builder.Services.AddScoped<IReportOrchestrator, ReportOrchestrator>();
|
builder.Services.AddScoped<IReportOrchestrator, ReportOrchestrator>();
|
||||||
|
|||||||
@@ -47,7 +47,7 @@ public class AnagraficaSectionRenderer : IPdfSectionRenderer
|
|||||||
y += SectionGap;
|
y += SectionGap;
|
||||||
|
|
||||||
// ── SEZIONE C: SOTTOSTANTI ────────────────────────────────────
|
// ── SEZIONE C: SOTTOSTANTI ────────────────────────────────────
|
||||||
if (info.Sottostanti.Count > 0)
|
if (!data.ShowDividend && info.Sottostanti.Count > 0)
|
||||||
{
|
{
|
||||||
// Se lo spazio rimanente è meno di 80pt, nuova pagina
|
// Se lo spazio rimanente è meno di 80pt, nuova pagina
|
||||||
if (y > PageH - 80f)
|
if (y > PageH - 80f)
|
||||||
@@ -330,14 +330,14 @@ public class AnagraficaSectionRenderer : IPdfSectionRenderer
|
|||||||
var rightItems = new (string Label, string Value)[]
|
var rightItems = new (string Label, string Value)[]
|
||||||
{
|
{
|
||||||
("Rend. Capitale a Scadenza", info.CapitalReturnAtMaturity ?? "-"),
|
("Rend. Capitale a Scadenza", info.CapitalReturnAtMaturity ?? "-"),
|
||||||
("IRR", info.IRR ?? "-"),
|
("Tasso Rendimento Interno", info.IRR ?? "-"),
|
||||||
("Protezione Capitale", info.BufferKProt ?? "-"),
|
("Protezione Capitale", info.BufferKProt ?? "-"),
|
||||||
("Protezione Coupon", info.BufferCPNProt ?? "-"),
|
("Protezione Cedola", info.BufferCPNProt ?? "-"),
|
||||||
("Valore Autocall", info.AutocallValue ?? "-"),
|
("Valore Richiamo Anticipato", info.AutocallValue ?? "-"),
|
||||||
("Distanza Autocall", info.TriggerAutocallDistance ?? "-"),
|
("Distanza Richiamo Anticipato", info.TriggerAutocallDistance ?? "-"),
|
||||||
("Rendimento Autocall", info.AutocallReturn ?? "-"),
|
("Rendimento Richiamo Anticipato", info.AutocallReturn ?? "-"),
|
||||||
("Fattore Airbag", string.IsNullOrWhiteSpace(info.FattoreAirbag) ? "—" : info.FattoreAirbag),
|
("Fattore Airbag", string.IsNullOrWhiteSpace(info.FattoreAirbag) ? "—" : info.FattoreAirbag),
|
||||||
("Trigger OneStar", string.IsNullOrWhiteSpace(info.TriggerOneStar) ? "—" : info.TriggerOneStar),
|
("Livello OneStar", string.IsNullOrWhiteSpace(info.TriggerOneStar) ? "—" : info.TriggerOneStar),
|
||||||
};
|
};
|
||||||
|
|
||||||
float leftY = DrawKVList(g, leftItems, 0, ColW, y);
|
float leftY = DrawKVList(g, leftItems, 0, ColW, y);
|
||||||
@@ -390,8 +390,8 @@ public class AnagraficaSectionRenderer : IPdfSectionRenderer
|
|||||||
grid.Style.CellPadding = new PdfPaddings(2, 2, 2, 2);
|
grid.Style.CellPadding = new PdfPaddings(2, 2, 2, 2);
|
||||||
|
|
||||||
// 9 colonne
|
// 9 colonne
|
||||||
string[] headers = { "Nome", "Strike", "Last", "% Perf.", "Barriera Capitale",
|
string[] headers = { "Nome", "Livello Iniziale", "Ultimo Prezzo", "% Perf.", "Barriera Capitale",
|
||||||
"Buffer Capitale", "Trigger Cedola", "Buffer Cedola", "Trigger Autocall" };
|
"Protezione Capitale", "Livello Cedola", "Protezione Cedola", "Livello Richiamo Anticipato" };
|
||||||
|
|
||||||
foreach (var _ in headers) grid.Columns.Add();
|
foreach (var _ in headers) grid.Columns.Add();
|
||||||
|
|
||||||
@@ -400,7 +400,7 @@ public class AnagraficaSectionRenderer : IPdfSectionRenderer
|
|||||||
for (int i = 0; i < headers.Length; i++)
|
for (int i = 0; i < headers.Length; i++)
|
||||||
{
|
{
|
||||||
hr.Cells[i].Value = headers[i];
|
hr.Cells[i].Value = headers[i];
|
||||||
hr.Cells[i].Style.Font = PdfTheme.TableBold;
|
hr.Cells[i].Style.Font = PdfTheme.Header;
|
||||||
hr.Cells[i].Style.BackgroundBrush = PdfTheme.TableHeaderBrush;
|
hr.Cells[i].Style.BackgroundBrush = PdfTheme.TableHeaderBrush;
|
||||||
hr.Cells[i].Style.TextBrush = PdfTheme.HeaderTextBrush;
|
hr.Cells[i].Style.TextBrush = PdfTheme.HeaderTextBrush;
|
||||||
hr.Cells[i].StringFormat = new PdfStringFormat(
|
hr.Cells[i].StringFormat = new PdfStringFormat(
|
||||||
@@ -425,7 +425,7 @@ public class AnagraficaSectionRenderer : IPdfSectionRenderer
|
|||||||
|
|
||||||
for (int c = 0; c < headers.Length; c++)
|
for (int c = 0; c < headers.Length; c++)
|
||||||
{
|
{
|
||||||
row.Cells[c].Style.Font = PdfTheme.TableFont;
|
row.Cells[c].Style.Font = PdfTheme.Small;
|
||||||
row.Cells[c].StringFormat = new PdfStringFormat(
|
row.Cells[c].StringFormat = new PdfStringFormat(
|
||||||
c == 0 ? PdfTextAlignment.Left : PdfTextAlignment.Right);
|
c == 0 ? PdfTextAlignment.Left : PdfTextAlignment.Right);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,234 @@
|
|||||||
|
using CertReports.Syncfusion.Helpers;
|
||||||
|
using CertReports.Syncfusion.Models;
|
||||||
|
using Syncfusion.Drawing;
|
||||||
|
using Syncfusion.Pdf;
|
||||||
|
using Syncfusion.Pdf.Graphics;
|
||||||
|
using Syncfusion.Pdf.Grid;
|
||||||
|
|
||||||
|
namespace CertReports.Syncfusion.Services.Implementations;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Genera la pagina landscape con la tabella unificata Sottostanti + Dividendi.
|
||||||
|
/// Non implementa IPdfSectionRenderer — iniettato direttamente nell'orchestratore.
|
||||||
|
/// </summary>
|
||||||
|
public class DividendSectionRenderer
|
||||||
|
{
|
||||||
|
// Larghezze colonne indicative (totale ~742pt) — scalate proporzionalmente a runtime
|
||||||
|
private static readonly float[] ColWidths =
|
||||||
|
[
|
||||||
|
82f, // 0 Nome
|
||||||
|
50f, // 1 Livello Iniziale
|
||||||
|
50f, // 2 Ultimo Prezzo
|
||||||
|
40f, // 3 % Perf.
|
||||||
|
50f, // 4 Barriera Capitale
|
||||||
|
46f, // 5 Protezione Capitale
|
||||||
|
50f, // 6 Livello Cedola
|
||||||
|
46f, // 7 Protezione Cedola
|
||||||
|
46f, // 8 Livello Richiamo Anticipato
|
||||||
|
50f, // 9 Data Stacco
|
||||||
|
50f, // 10 Data Pagamento
|
||||||
|
40f, // 11 Importo
|
||||||
|
54f, // 12 Rendimento (+14)
|
||||||
|
40f, // 13 Importo Futuro
|
||||||
|
54f, // 14 Rendimento Futuro (+14)
|
||||||
|
];
|
||||||
|
|
||||||
|
private static readonly string[] Col2Headers =
|
||||||
|
[
|
||||||
|
"", // Nome (nessuna sub-label)
|
||||||
|
"Livello Iniziale", // Strike
|
||||||
|
"Ultimo Prezzo", // Last
|
||||||
|
"% Perf.", // % Perf. (invariato)
|
||||||
|
"Barriera Capitale", // Barr.Cap.
|
||||||
|
"Protezione Capitale", // Buf.Cap.
|
||||||
|
"Livello Cedola", // Trig.CPN
|
||||||
|
"Protezione Cedola", // Buf.CPN
|
||||||
|
"Livello Richiamo Anticipato", // Trig.AC
|
||||||
|
"Data Stacco", // (invariato)
|
||||||
|
"Data Pagamento", // Data Pag.
|
||||||
|
"Importo", // (invariato)
|
||||||
|
"Rendimento", // Rend.
|
||||||
|
"Importo Futuro", // Imp.Fut.
|
||||||
|
"Rendimento Futuro", // Rend.Fut.
|
||||||
|
];
|
||||||
|
|
||||||
|
// Indici colonne soggette a colore performance (0-based)
|
||||||
|
private static readonly HashSet<int> PerfCols = [3, 5, 7, 12, 14];
|
||||||
|
|
||||||
|
// Static brush e pen per header
|
||||||
|
private static readonly PdfSolidBrush DarkBlueBrush = new(Color.FromArgb(255, 10, 56, 128)); // #0A3880
|
||||||
|
private static readonly PdfPen SeparatorPen = new(Color.FromArgb(255, 100, 181, 246), 1.5f); // #64B5F6
|
||||||
|
|
||||||
|
public PdfDocument Render(CertificateReportData data)
|
||||||
|
{
|
||||||
|
var doc = new PdfDocument();
|
||||||
|
doc.PageSettings.Orientation = PdfPageOrientation.Landscape;
|
||||||
|
doc.PageSettings.Size = PdfPageSize.A4;
|
||||||
|
|
||||||
|
var page = doc.Pages.Add();
|
||||||
|
var g = page.Graphics;
|
||||||
|
var size = page.GetClientSize();
|
||||||
|
|
||||||
|
float pageWidth = size.Width;
|
||||||
|
float pageHeight = size.Height;
|
||||||
|
|
||||||
|
float w = pageWidth - 2 * PdfTheme.PageMargin;
|
||||||
|
float h = pageHeight - 2 * PdfTheme.PageMargin - PdfTheme.FooterHeight;
|
||||||
|
float x0 = PdfTheme.PageMargin;
|
||||||
|
float y = PdfTheme.PageMargin;
|
||||||
|
|
||||||
|
// ── Titolo stile DrawSectionLabel ─────────────────────────────
|
||||||
|
g.DrawRectangle(PdfTheme.AccentBlueBrush, new RectangleF(x0, y, 3f, 14f));
|
||||||
|
g.DrawString("Sottostanti e Dividendi", PdfTheme.Bold,
|
||||||
|
new PdfSolidBrush(PdfTheme.AccentBlue),
|
||||||
|
new RectangleF(x0 + 6f, y, w - 6f, 14f));
|
||||||
|
y += 16f;
|
||||||
|
|
||||||
|
// ── Calcolo fattore scala su larghezza reale ──────────────────
|
||||||
|
float totalNominal = ColWidths.Sum();
|
||||||
|
float scale = w / totalNominal;
|
||||||
|
float[] cw = ColWidths.Select(c => c * scale).ToArray();
|
||||||
|
|
||||||
|
// ── Disegno header a 2 livelli ────────────────────────────────
|
||||||
|
float rh = PdfTheme.RowHeight;
|
||||||
|
|
||||||
|
DrawHeader(g, x0, y, cw, rh);
|
||||||
|
|
||||||
|
y += rh * 3;
|
||||||
|
|
||||||
|
// ── Tabella dati ──────────────────────────────────────────────
|
||||||
|
DrawDataGrid(g, data, x0, y, cw);
|
||||||
|
|
||||||
|
// ── Footer ────────────────────────────────────────────────────
|
||||||
|
PdfTheme.DrawFooter(g, pageWidth, pageHeight, 1, data.ShowBranding);
|
||||||
|
|
||||||
|
return doc;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void DrawHeader(PdfGraphics g, float x0, float y, float[] cw, float rh)
|
||||||
|
{
|
||||||
|
var headerFont = PdfTheme.Header;
|
||||||
|
var whiteBrush = PdfBrushes.White;
|
||||||
|
var accentBrush = PdfTheme.AccentBlueBrush;
|
||||||
|
|
||||||
|
// ── Riga 1: Gruppi ─────────────────────────────────────────────
|
||||||
|
float cx = x0;
|
||||||
|
|
||||||
|
// "Nome" (col 0) — cella vuota
|
||||||
|
DrawHeaderCell(g, cx, y, cw[0], rh, "", accentBrush, whiteBrush, headerFont);
|
||||||
|
cx += cw[0];
|
||||||
|
|
||||||
|
// "SOTTOSTANTE" (cols 1-3)
|
||||||
|
float sottostanteW = cw[1] + cw[2] + cw[3];
|
||||||
|
DrawHeaderCell(g, cx, y, sottostanteW, rh, "SOTTOSTANTE", accentBrush, whiteBrush, headerFont);
|
||||||
|
cx += sottostanteW;
|
||||||
|
|
||||||
|
// "BARRIERE" (cols 4-8)
|
||||||
|
float barriereW = cw[4] + cw[5] + cw[6] + cw[7] + cw[8];
|
||||||
|
DrawHeaderCell(g, cx, y, barriereW, rh, "BARRIERE", accentBrush, whiteBrush, headerFont);
|
||||||
|
cx += barriereW;
|
||||||
|
|
||||||
|
// "DIVIDENDI" (cols 9-14)
|
||||||
|
float dividendiW = cw[9] + cw[10] + cw[11] + cw[12] + cw[13] + cw[14];
|
||||||
|
DrawHeaderCell(g, cx, y, dividendiW, rh, "DIVIDENDI", DarkBlueBrush, whiteBrush, headerFont);
|
||||||
|
|
||||||
|
// ── Riga 2: Sottocolonne ───────────────────────────────────────
|
||||||
|
cx = x0;
|
||||||
|
for (int i = 0; i < cw.Length; i++)
|
||||||
|
{
|
||||||
|
var bg = i >= 9 ? DarkBlueBrush : accentBrush;
|
||||||
|
DrawHeaderCell(g, cx, y + rh, cw[i], rh * 2, Col2Headers[i], bg, whiteBrush, headerFont);
|
||||||
|
cx += cw[i];
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Separatore verticale tra col 8 e col 9 ────────────────────
|
||||||
|
float sepX = x0 + cw.Take(9).Sum();
|
||||||
|
g.DrawLine(SeparatorPen, new PointF(sepX, y), new PointF(sepX, y + rh * 3));
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void DrawHeaderCell(PdfGraphics g, float x, float y, float w, float h,
|
||||||
|
string text, PdfBrush bg, PdfBrush fg, PdfFont font)
|
||||||
|
{
|
||||||
|
g.DrawRectangle(bg, new RectangleF(x, y, w, h));
|
||||||
|
if (!string.IsNullOrEmpty(text))
|
||||||
|
{
|
||||||
|
var fmt = new PdfStringFormat
|
||||||
|
{
|
||||||
|
Alignment = PdfTextAlignment.Center,
|
||||||
|
LineAlignment = PdfVerticalAlignment.Middle,
|
||||||
|
};
|
||||||
|
g.DrawString(text, font, fg, new RectangleF(x + 1, y, w - 2, h), fmt);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void DrawDataGrid(PdfGraphics g, CertificateReportData data,
|
||||||
|
float x0, float y, float[] cw)
|
||||||
|
{
|
||||||
|
var grid = new PdfGrid();
|
||||||
|
grid.Style.CellPadding = new PdfPaddings(2, 2, 2, 2);
|
||||||
|
grid.Style.Font = PdfTheme.Small;
|
||||||
|
|
||||||
|
// 15 colonne
|
||||||
|
grid.Columns.Add(15);
|
||||||
|
for (int i = 0; i < 15; i++)
|
||||||
|
grid.Columns[i].Width = cw[i];
|
||||||
|
|
||||||
|
var sottostanti = data.Info.Sottostanti;
|
||||||
|
|
||||||
|
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 = string.IsNullOrEmpty(s.Performance) ? "—" : s.Performance;
|
||||||
|
row.Cells[4].Value = string.IsNullOrEmpty(s.CapitalBarrier) ? "—" : s.CapitalBarrier;
|
||||||
|
row.Cells[5].Value = string.IsNullOrEmpty(s.ULCapitalBarrierBuffer) ? "—" : s.ULCapitalBarrierBuffer;
|
||||||
|
row.Cells[6].Value = string.IsNullOrEmpty(s.CouponBarrier) ? "—" : s.CouponBarrier;
|
||||||
|
row.Cells[7].Value = string.IsNullOrEmpty(s.ULCouponBarrierBuffer) ? "—" : s.ULCouponBarrierBuffer;
|
||||||
|
row.Cells[8].Value = string.IsNullOrEmpty(s.TriggerAutocall) ? "—" : s.TriggerAutocall;
|
||||||
|
row.Cells[9].Value = string.IsNullOrEmpty(s.DividendExDate) ? "—" : s.DividendExDate;
|
||||||
|
row.Cells[10].Value = string.IsNullOrEmpty(s.DividendPayDate) ? "—" : s.DividendPayDate;
|
||||||
|
row.Cells[11].Value = string.IsNullOrEmpty(s.DividendAmount) ? "—" : s.DividendAmount;
|
||||||
|
row.Cells[12].Value = string.IsNullOrEmpty(s.DividendYield) ? "—" : s.DividendYield;
|
||||||
|
row.Cells[13].Value = string.IsNullOrEmpty(s.DividendFutAmount) ? "—" : s.DividendFutAmount;
|
||||||
|
row.Cells[14].Value = string.IsNullOrEmpty(s.DividendFutYield) ? "—" : s.DividendFutYield;
|
||||||
|
|
||||||
|
// Righe alternate
|
||||||
|
if (i % 2 == 1)
|
||||||
|
{
|
||||||
|
for (int c = 0; c < 15; c++)
|
||||||
|
row.Cells[c].Style.BackgroundBrush = PdfTheme.TableAltRowBrush;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Colore performance: negativi rosso, positivi verde
|
||||||
|
foreach (int c in PerfCols)
|
||||||
|
{
|
||||||
|
var val = row.Cells[c].Value?.ToString();
|
||||||
|
if (string.IsNullOrEmpty(val) || val == "—") continue;
|
||||||
|
|
||||||
|
bool isNegative = val.TrimStart().StartsWith('-');
|
||||||
|
row.Cells[c].Style.TextBrush = isNegative
|
||||||
|
? PdfTheme.NegativeRedBrush
|
||||||
|
: PdfTheme.PositiveGreenBrush;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Allineamento: Nome a sinistra, tutto il resto centrato
|
||||||
|
row.Cells[0].StringFormat = new PdfStringFormat(PdfTextAlignment.Left);
|
||||||
|
for (int c = 1; c < 15; c++)
|
||||||
|
{
|
||||||
|
row.Cells[c].StringFormat = new PdfStringFormat
|
||||||
|
{
|
||||||
|
Alignment = PdfTextAlignment.Center,
|
||||||
|
LineAlignment = PdfVerticalAlignment.Middle,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Pagina singola: specifica garantisce max ~20 sottostanti per certificato
|
||||||
|
PdfTheme.ApplyThinBorders(grid);
|
||||||
|
grid.Draw(g, new PointF(x0, y));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -30,9 +30,12 @@ public class EventiSectionRenderer : IPdfSectionRenderer
|
|||||||
float y = 0;
|
float y = 0;
|
||||||
|
|
||||||
// ── Titolo ─────────────────────────────────────────────────────
|
// ── Titolo ─────────────────────────────────────────────────────
|
||||||
g.DrawString("Lista Eventi", PdfTheme.SectionTitleFont,
|
// Accent line verticale sinistra (3pt wide, 14pt tall) + testo blue su sfondo bianco
|
||||||
new PdfSolidBrush(PdfTheme.SectionTitle), new RectangleF(0, y, w, 18));
|
g.DrawRectangle(PdfTheme.AccentBlueBrush, new RectangleF(0, y, 3f, 14f));
|
||||||
y += 22;
|
g.DrawString("Lista Eventi", PdfTheme.Bold,
|
||||||
|
new PdfSolidBrush(PdfTheme.AccentBlue),
|
||||||
|
new RectangleF(6f, y, w, 14f));
|
||||||
|
y += 16f;
|
||||||
|
|
||||||
// ── Griglia ────────────────────────────────────────────────────
|
// ── Griglia ────────────────────────────────────────────────────
|
||||||
var grid = new PdfGrid();
|
var grid = new PdfGrid();
|
||||||
@@ -48,12 +51,12 @@ public class EventiSectionRenderer : IPdfSectionRenderer
|
|||||||
if (isExpired)
|
if (isExpired)
|
||||||
{
|
{
|
||||||
// Certificati non in quotazione: senza Ex Date e Record,
|
// Certificati non in quotazione: senza Ex Date e Record,
|
||||||
// rinominati Trigger Cedola→Barriera Cedola e Trigger Autocall→Soglia Rimborso
|
// rinominati Trigger Cedola→Barriera Cedola e Trigger Autocall→Livello Richiamo Anticipato
|
||||||
headers = new[]
|
headers = new[]
|
||||||
{
|
{
|
||||||
"Osservazione", "Pagamento",
|
"Osservazione", "Pagamento",
|
||||||
"Barriera Cedola", "Cedola %", "Pagato", "Memoria",
|
"Barriera Cedola", "Cedola %", "Pagato", "Memoria",
|
||||||
"Importo Pagato", "Soglia Rimborso", "Valore Autocall"
|
"Importo Pagato", "Soglia Rimborso", "Valore Richiamo Anticipato"
|
||||||
};
|
};
|
||||||
cw = new float[] { 62, 58, 52, 46, 36, 46, 52, 58, 52 };
|
cw = new float[] { 62, 58, 52, 46, 36, 46, 52, 58, 52 };
|
||||||
paidColIndex = 4;
|
paidColIndex = 4;
|
||||||
@@ -63,8 +66,8 @@ public class EventiSectionRenderer : IPdfSectionRenderer
|
|||||||
headers = new[]
|
headers = new[]
|
||||||
{
|
{
|
||||||
"Osservazione", "Ex Date", "Record", "Pagamento",
|
"Osservazione", "Ex Date", "Record", "Pagamento",
|
||||||
"Trigger Cedola", "Cedola %", "Pagato", "Memoria",
|
"Livello Cedola", "Cedola %", "Pagato", "Memoria",
|
||||||
"Importo Pagato", "Trigger Autocall", "Valore Autocall"
|
"Importo Pagato", "Livello Richiamo Anticipato", "Valore Richiamo Anticipato"
|
||||||
};
|
};
|
||||||
cw = new float[] { 62, 52, 52, 58, 52, 46, 36, 46, 52, 58, 52 };
|
cw = new float[] { 62, 52, 52, 58, 52, 46, 36, 46, 52, 58, 52 };
|
||||||
paidColIndex = 6;
|
paidColIndex = 6;
|
||||||
|
|||||||
@@ -46,7 +46,7 @@ public class ExpiredAnagraficaSectionRenderer : IPdfSectionRenderer
|
|||||||
y += SectionGap;
|
y += SectionGap;
|
||||||
|
|
||||||
// ── SEZIONE C: SOTTOSTANTI ────────────────────────────────────
|
// ── SEZIONE C: SOTTOSTANTI ────────────────────────────────────
|
||||||
if (info.Sottostanti.Count > 0)
|
if (!data.ShowDividend && info.Sottostanti.Count > 0)
|
||||||
{
|
{
|
||||||
if (y > PageH - 80f)
|
if (y > PageH - 80f)
|
||||||
{
|
{
|
||||||
@@ -229,8 +229,8 @@ public class ExpiredAnagraficaSectionRenderer : IPdfSectionRenderer
|
|||||||
var grid = new PdfGrid();
|
var grid = new PdfGrid();
|
||||||
grid.Style.CellPadding = new PdfPaddings(2, 2, 2, 2);
|
grid.Style.CellPadding = new PdfPaddings(2, 2, 2, 2);
|
||||||
|
|
||||||
string[] headers = { "Nome", "Strike", "Last", "% Perf.", "Barriera Capitale",
|
string[] headers = { "Nome", "Livello Iniziale", "Ultimo Prezzo", "% Perf.", "Barriera Capitale",
|
||||||
"Buffer Capitale", "Trigger Cedola", "Buffer Cedola", "Trigger Autocall" };
|
"Protezione Capitale", "Livello Cedola", "Protezione Cedola", "Livello Richiamo Anticipato" };
|
||||||
|
|
||||||
foreach (var _ in headers) grid.Columns.Add();
|
foreach (var _ in headers) grid.Columns.Add();
|
||||||
|
|
||||||
@@ -238,7 +238,7 @@ public class ExpiredAnagraficaSectionRenderer : IPdfSectionRenderer
|
|||||||
for (int i = 0; i < headers.Length; i++)
|
for (int i = 0; i < headers.Length; i++)
|
||||||
{
|
{
|
||||||
hr.Cells[i].Value = headers[i];
|
hr.Cells[i].Value = headers[i];
|
||||||
hr.Cells[i].Style.Font = PdfTheme.TableBold;
|
hr.Cells[i].Style.Font = PdfTheme.Header;
|
||||||
hr.Cells[i].Style.BackgroundBrush = PdfTheme.TableHeaderBrush;
|
hr.Cells[i].Style.BackgroundBrush = PdfTheme.TableHeaderBrush;
|
||||||
hr.Cells[i].Style.TextBrush = PdfTheme.HeaderTextBrush;
|
hr.Cells[i].Style.TextBrush = PdfTheme.HeaderTextBrush;
|
||||||
hr.Cells[i].StringFormat = new PdfStringFormat(
|
hr.Cells[i].StringFormat = new PdfStringFormat(
|
||||||
@@ -262,7 +262,7 @@ public class ExpiredAnagraficaSectionRenderer : IPdfSectionRenderer
|
|||||||
|
|
||||||
for (int c = 0; c < headers.Length; c++)
|
for (int c = 0; c < headers.Length; c++)
|
||||||
{
|
{
|
||||||
row.Cells[c].Style.Font = PdfTheme.TableFont;
|
row.Cells[c].Style.Font = PdfTheme.Small;
|
||||||
row.Cells[c].StringFormat = new PdfStringFormat(
|
row.Cells[c].StringFormat = new PdfStringFormat(
|
||||||
c == 0 ? PdfTextAlignment.Left : PdfTextAlignment.Right);
|
c == 0 ? PdfTextAlignment.Left : PdfTextAlignment.Right);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ public class ReportOrchestrator : IReportOrchestrator
|
|||||||
private readonly IPdfCacheService _cache;
|
private readonly IPdfCacheService _cache;
|
||||||
private readonly ILogger<ReportOrchestrator> _logger;
|
private readonly ILogger<ReportOrchestrator> _logger;
|
||||||
private readonly ExpiredAnagraficaSectionRenderer _expiredAnagraficaRenderer;
|
private readonly ExpiredAnagraficaSectionRenderer _expiredAnagraficaRenderer;
|
||||||
|
private readonly DividendSectionRenderer _dividendRenderer;
|
||||||
|
|
||||||
public ReportOrchestrator(
|
public ReportOrchestrator(
|
||||||
ICertificateDataService dataService,
|
ICertificateDataService dataService,
|
||||||
@@ -30,7 +31,8 @@ public class ReportOrchestrator : IReportOrchestrator
|
|||||||
IPdfMergerService merger,
|
IPdfMergerService merger,
|
||||||
IPdfCacheService cache,
|
IPdfCacheService cache,
|
||||||
ILogger<ReportOrchestrator> logger,
|
ILogger<ReportOrchestrator> logger,
|
||||||
ExpiredAnagraficaSectionRenderer expiredAnagraficaRenderer)
|
ExpiredAnagraficaSectionRenderer expiredAnagraficaRenderer,
|
||||||
|
DividendSectionRenderer dividendRenderer)
|
||||||
{
|
{
|
||||||
_dataService = dataService;
|
_dataService = dataService;
|
||||||
_sectionRenderers = sectionRenderers;
|
_sectionRenderers = sectionRenderers;
|
||||||
@@ -39,13 +41,15 @@ public class ReportOrchestrator : IReportOrchestrator
|
|||||||
_cache = cache;
|
_cache = cache;
|
||||||
_logger = logger;
|
_logger = logger;
|
||||||
_expiredAnagraficaRenderer = expiredAnagraficaRenderer;
|
_expiredAnagraficaRenderer = expiredAnagraficaRenderer;
|
||||||
|
_dividendRenderer = dividendRenderer;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<byte[]> GenerateReportAsync(string isin, bool showBranding = false)
|
public async Task<byte[]> GenerateReportAsync(string isin, bool showBranding = false, bool showDividend = false)
|
||||||
{
|
{
|
||||||
// ── Cache check ────────────────────────────────────────────────
|
// ── Cache check ────────────────────────────────────────────────
|
||||||
var baseCacheKey = showBranding ? $"{isin}:branded" : isin;
|
var dividendSuffix = showDividend ? ":dividend" : "";
|
||||||
var expiredCacheKey = showBranding ? $"{isin}:expired:branded" : $"{isin}:expired";
|
var baseCacheKey = showBranding ? $"{isin}:branded{dividendSuffix}" : $"{isin}{dividendSuffix}";
|
||||||
|
var expiredCacheKey = showBranding ? $"{isin}:expired:branded{dividendSuffix}" : $"{isin}:expired{dividendSuffix}";
|
||||||
|
|
||||||
var cached = _cache.Get(baseCacheKey) ?? _cache.Get(expiredCacheKey);
|
var cached = _cache.Get(baseCacheKey) ?? _cache.Get(expiredCacheKey);
|
||||||
if (cached != null)
|
if (cached != null)
|
||||||
@@ -63,6 +67,7 @@ public class ReportOrchestrator : IReportOrchestrator
|
|||||||
Eventi = await _dataService.GetCertificateEventsAsync(isin),
|
Eventi = await _dataService.GetCertificateEventsAsync(isin),
|
||||||
Scenario = await _dataService.GetScenarioAnalysisAsync(isin),
|
Scenario = await _dataService.GetScenarioAnalysisAsync(isin),
|
||||||
ShowBranding = showBranding,
|
ShowBranding = showBranding,
|
||||||
|
ShowDividend = showDividend,
|
||||||
};
|
};
|
||||||
|
|
||||||
// ── 2. Determina il tipo di report ────────────────────────────
|
// ── 2. Determina il tipo di report ────────────────────────────
|
||||||
@@ -95,6 +100,20 @@ public class ReportOrchestrator : IReportOrchestrator
|
|||||||
throw;
|
throw;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (reportData.ShowDividend)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
pdfSections.Add(_dividendRenderer.Render(reportData));
|
||||||
|
_logger.LogInformation("Sezione 'Dividend' generata per {Isin}", isin);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogError(ex, "Errore nella sezione 'Dividend' per {Isin}", isin);
|
||||||
|
throw;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
var eventiRenderer = _sectionRenderers.First(r => r.SectionName == "Eventi");
|
var eventiRenderer = _sectionRenderers.First(r => r.SectionName == "Eventi");
|
||||||
@@ -130,6 +149,21 @@ public class ReportOrchestrator : IReportOrchestrator
|
|||||||
_logger.LogError(ex, "Errore nella sezione '{Section}' per {Isin}", renderer.SectionName, isin);
|
_logger.LogError(ex, "Errore nella sezione '{Section}' per {Isin}", renderer.SectionName, isin);
|
||||||
throw;
|
throw;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Inserire pagina dividend subito dopo Anagrafica (Sezione 1)
|
||||||
|
if (renderer.SectionName == "Anagrafica" && reportData.ShowDividend)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
pdfSections.Add(_dividendRenderer.Render(reportData));
|
||||||
|
_logger.LogInformation("Sezione 'Dividend' generata per {Isin}", isin);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogError(ex, "Errore nella sezione 'Dividend' per {Isin}", isin);
|
||||||
|
throw;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -43,13 +43,13 @@ public class ScenarioSectionRenderer : IPdfSectionRenderer
|
|||||||
float w = page.GetClientSize().Width;
|
float w = page.GetClientSize().Width;
|
||||||
float y = 0;
|
float y = 0;
|
||||||
|
|
||||||
// ── Titolo centrato ────────────────────────────────────────────
|
// ── Titolo ────────────────────────────────────────────────────
|
||||||
var titleFont = new PdfStandardFont(PdfFontFamily.Helvetica, 16f, PdfFontStyle.Bold);
|
// Accent line verticale sinistra (3pt wide, 14pt tall) + testo blue su sfondo bianco
|
||||||
g.DrawString("Analisi Scenario", titleFont,
|
g.DrawRectangle(PdfTheme.AccentBlueBrush, new RectangleF(0, y, 3f, 14f));
|
||||||
new PdfSolidBrush(Color.FromArgb(255, 46, 80, 144)),
|
g.DrawString("Analisi Scenario", PdfTheme.Bold,
|
||||||
new RectangleF(0, y, w, 28),
|
new PdfSolidBrush(PdfTheme.AccentBlue),
|
||||||
new PdfStringFormat(PdfTextAlignment.Center));
|
new RectangleF(6f, y, w, 14f));
|
||||||
y += 40;
|
y += 16f;
|
||||||
|
|
||||||
var scenario = data.Scenario;
|
var scenario = data.Scenario;
|
||||||
if (scenario.Rows.Count == 0)
|
if (scenario.Rows.Count == 0)
|
||||||
|
|||||||
@@ -46,5 +46,5 @@ public interface IPdfMergerService
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public interface IReportOrchestrator
|
public interface IReportOrchestrator
|
||||||
{
|
{
|
||||||
Task<byte[]> GenerateReportAsync(string isin, bool showBranding = false);
|
Task<byte[]> GenerateReportAsync(string isin, bool showBranding = false, bool showDividend = false);
|
||||||
}
|
}
|
||||||
|
|||||||
149
docs/superpowers/specs/2026-03-23-dividend-section-design.md
Normal file
149
docs/superpowers/specs/2026-03-23-dividend-section-design.md
Normal file
@@ -0,0 +1,149 @@
|
|||||||
|
# Dividend Section — Design Spec
|
||||||
|
**Data:** 2026-03-23
|
||||||
|
**Progetto:** SmartReports / CertReports.Syncfusion
|
||||||
|
**Stato:** Approvato
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Obiettivo
|
||||||
|
|
||||||
|
Aggiungere il supporto ai dati dividendi dei sottostanti nel report PDF, attivabile tramite il parametro query `?dividend=true`. Quando attivo, i dati dividendi (già presenti nella SP `rpt_Details_UL_ISIN`) vengono visualizzati in una pagina landscape dedicata con tabella unificata Sottostanti+Dividendi a header raggruppati.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Parametro API
|
||||||
|
|
||||||
|
```
|
||||||
|
?dividend=true → attiva la pagina dividendi
|
||||||
|
?dividend=false → comportamento invariato (default)
|
||||||
|
```
|
||||||
|
|
||||||
|
Disponibile su tutti gli endpoint report esistenti (`/by-isin/{isin}`, `?p=`, `?alias=`, `/download`).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Comportamento
|
||||||
|
|
||||||
|
### Con `dividend=false` (default)
|
||||||
|
Il report è identico a quello attuale. Nessuna modifica visibile.
|
||||||
|
|
||||||
|
### Con `dividend=true`
|
||||||
|
1. La tabella Sottostanti **non viene renderizzata** in Sezione 1 (né in `AnagraficaSectionRenderer` né in `ExpiredAnagraficaSectionRenderer`).
|
||||||
|
2. Dopo la Sezione 1 (Anagrafica) e prima della Sezione 2 (Eventi) viene inserita una **pagina landscape dedicata** con la tabella unificata Sottostanti+Dividendi.
|
||||||
|
3. Il resto del report (Eventi, Scenario, Grafico) rimane invariato.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Struttura tabella landscape
|
||||||
|
|
||||||
|
Pagina A4 orizzontale (landscape), margini standard (`PdfTheme.PageMargin`).
|
||||||
|
Larghezza utile calcolata a runtime: `var size = page.GetClientSize(); float w = size.Width - 2 * PdfTheme.PageMargin; float h = size.Height - 2 * PdfTheme.PageMargin - PdfTheme.FooterHeight;` — NON usare costanti hardcoded. Font: `PdfTheme.TableFont` (7pt).
|
||||||
|
|
||||||
|
### Header a 2 livelli (colspan)
|
||||||
|
|
||||||
|
| Riga 1 | Nome | SOTTOSTANTE (×3) | BARRIERE (×5) | DIVIDENDI (×6) |
|
||||||
|
|--------|------|------------------|---------------|----------------|
|
||||||
|
| Riga 2 | — | Strike, Last, % Perf. | Barr.Cap., Buf.Cap., Trig.CPN, Buf.CPN, Trig.AC | Data Stacco, Data Pag., Importo, Rend., Imp.Fut., Rend.Fut. |
|
||||||
|
|
||||||
|
- Gruppo **SOTTOSTANTE** + **BARRIERE**: sfondo `#1565C0` (AccentBlue)
|
||||||
|
- Gruppo **DIVIDENDI**: sfondo `#0A3880` (blu scuro) per differenziazione visiva
|
||||||
|
- Separatore verticale blu (`#64B5F6`) tra colonna T.AC e colonna Data Stacco
|
||||||
|
- Colori performance: negativi `NegativeRed`, positivi `PositiveGreen`
|
||||||
|
- Righe alternate: `TableAltRow`
|
||||||
|
|
||||||
|
### Larghezze colonne indicative (totale ~770pt)
|
||||||
|
|
||||||
|
| Colonna | Width (pt) |
|
||||||
|
|---------|-----------|
|
||||||
|
| Nome | 90 |
|
||||||
|
| Strike | 50 |
|
||||||
|
| Last | 50 |
|
||||||
|
| % Perf. | 46 |
|
||||||
|
| Barr.Cap. | 50 |
|
||||||
|
| Buf.Cap. | 46 |
|
||||||
|
| Trig.CPN | 50 |
|
||||||
|
| Buf.CPN | 46 |
|
||||||
|
| Trig.AC | 46 |
|
||||||
|
| Data Stacco | 54 |
|
||||||
|
| Data Pag. | 54 |
|
||||||
|
| Importo | 40 |
|
||||||
|
| Rend. | 40 |
|
||||||
|
| Imp.Fut. | 40 |
|
||||||
|
| Rend.Fut. | 40 |
|
||||||
|
| **Totale** | **742** |
|
||||||
|
|
||||||
|
Le larghezze vengono scalate proporzionalmente: `float scale = w / total;` dove `w` proviene da `page.GetClientSize().Width - 2 * PdfTheme.PageMargin` (calcolato a runtime, non hardcoded).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Mapping dati
|
||||||
|
|
||||||
|
I campi dividendi sono già nel modello `Sottostante` (da SP `rpt_Details_UL_ISIN`):
|
||||||
|
|
||||||
|
| Colonna PDF | Campo modello |
|
||||||
|
|-------------|--------------|
|
||||||
|
| Data Stacco | `DividendExDate` |
|
||||||
|
| Data Pag. | `DividendPayDate` |
|
||||||
|
| Importo | `DividendAmount` |
|
||||||
|
| Rend. | `DividendYield` |
|
||||||
|
| Imp.Fut. | `DividendFutAmount` |
|
||||||
|
| Rend.Fut. | `DividendFutYield` |
|
||||||
|
|
||||||
|
Nessuna modifica al data service o alle stored procedure.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Componenti da modificare / creare
|
||||||
|
|
||||||
|
### 1. `CertificateReportData` (Models/CertificateModels.cs)
|
||||||
|
Aggiungere proprietà:
|
||||||
|
```csharp
|
||||||
|
public bool ShowDividend { get; set; } = false;
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. `ReportController` (Controllers/ReportController.cs)
|
||||||
|
Leggere il parametro `dividend` da query string e impostare `data.ShowDividend`.
|
||||||
|
|
||||||
|
### 3. `PdfCacheService`
|
||||||
|
La chiave cache deve includere il flag dividend per evitare collisioni:
|
||||||
|
- `{isin}` — no branding, no dividend
|
||||||
|
- `{isin}:branded` — branding, no dividend
|
||||||
|
- `{isin}:dividend` — no branding, dividend
|
||||||
|
- `{isin}:branded:dividend` — branding + dividend
|
||||||
|
|
||||||
|
### 4. `AnagraficaSectionRenderer`
|
||||||
|
Condizione nel metodo `Render()`: se `data.ShowDividend == true`, saltare il blocco "SEZIONE C: SOTTOSTANTI".
|
||||||
|
|
||||||
|
### 5. `ExpiredAnagraficaSectionRenderer`
|
||||||
|
Stessa condizione: se `data.ShowDividend == true`, saltare il blocco "SEZIONE C: SOTTOSTANTI".
|
||||||
|
|
||||||
|
### 6. `DividendSectionRenderer` (nuovo file)
|
||||||
|
- Classe: `DividendSectionRenderer`
|
||||||
|
- **Non** implementa `IPdfSectionRenderer` (non partecipa al ciclo ordinato)
|
||||||
|
- Registrazione: `AddScoped<DividendSectionRenderer>()` in `Program.cs`
|
||||||
|
- Metodo: `PdfDocument Render(CertificateReportData data)`
|
||||||
|
- Pagina landscape A4, titolo "Sottostanti e Dividendi", tabella con header a 2 livelli
|
||||||
|
- Dimensioni pagina: ricavare da `page.GetClientSize()` — `w = size.Width - 2 * PageMargin`, `h = size.Height - 2 * PageMargin - FooterHeight`
|
||||||
|
- Footer: `PdfTheme.DrawFooter(g, w, h, 1, data.ShowBranding)` — il numero pagina `1` è relativo alla pagina interna del documento parziale, coerente con lo stesso pattern usato da tutti gli altri renderer (la numerazione assoluta è una limitazione nota del sistema di merge)
|
||||||
|
|
||||||
|
### 7. `ReportOrchestrator`
|
||||||
|
- Aggiungere `DividendSectionRenderer dividendRenderer` come parametro del costruttore, accanto a `ExpiredAnagraficaSectionRenderer expiredRenderer` già presente — stesso pattern di iniezione diretta
|
||||||
|
- In entrambi i flussi (attivo ed expired): se `data.ShowDividend == true`, chiamare `dividendRenderer.Render(data)` e inserire il `PdfDocument` risultante nella lista documenti da mergiare **subito dopo** il documento della Sezione 1 (Anagrafica), prima di tutti gli altri
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Note implementative
|
||||||
|
|
||||||
|
- **Header colspan su PdfGrid**: Syncfusion `PdfGrid` non supporta nativamente colspan negli header. Implementare i due livelli di header come righe manuali disegnate con `DrawRectangle` + `DrawString` (seguire il pattern di `AnagraficaSectionRenderer.DrawEmittenteTable` per le altezze delle righe header: `rh = PdfTheme.RowHeight`). Riga 1 (gruppi): altezza `rh`; riga 2 (sotto-colonne): altezza `rh`. La `PdfGrid` dati viene disegnata a `y` iniziale = `y + rh * 2`. Le larghezze delle singole colonne nella riga 2 devono sommarsi esattamente alle larghezze dei gruppi in riga 1 per allineamento corretto.
|
||||||
|
- **Colori negativi/positivi**: applicare `ColorPerformanceCell` su % Perf., Buf.Cap., Buf.CPN, Rend., Rend.Fut. — stessa logica di `AnagraficaSectionRenderer`.
|
||||||
|
- **Valori assenti**: celle dividendo vuote mostrano `"—"`.
|
||||||
|
- Il parametro `dividend` è indipendente da `branding` — entrambi possono essere attivi contemporaneamente.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Non in scope
|
||||||
|
|
||||||
|
- Modifiche alle stored procedure o al data service
|
||||||
|
- Nuove colonne nel modello `Sottostante`
|
||||||
|
- Modifiche al grafico o agli eventi
|
||||||
|
- Paginazione automatica della tabella dividendi (assumiamo max ~20 sottostanti per certificato)
|
||||||
Reference in New Issue
Block a user