diff --git a/CertReports.Syncfusion/Controllers/FundReportController.cs b/CertReports.Syncfusion/Controllers/FundReportController.cs index aaadb8b..eb708e9 100644 --- a/CertReports.Syncfusion/Controllers/FundReportController.cs +++ b/CertReports.Syncfusion/Controllers/FundReportController.cs @@ -93,7 +93,7 @@ public class FundReportController : ControllerBase var pdfBytes = await _orchestrator.GenerateReportAsync(isin, branding); var disposition = inline ? "inline" : "attachment"; Response.Headers.Append("Content-Disposition", - $"{disposition}; filename=fund_{isin}.pdf"); + $"{disposition}; filename={isin}.pdf"); return File(pdfBytes, "application/pdf"); } catch (InvalidOperationException ex) when (ex.Message.Contains("Nessun dato")) diff --git a/CertReports.Syncfusion/Services/Implementations/FundAnagraficaRenderer.cs b/CertReports.Syncfusion/Services/Implementations/FundAnagraficaRenderer.cs index 89e21e8..b90b0fc 100644 --- a/CertReports.Syncfusion/Services/Implementations/FundAnagraficaRenderer.cs +++ b/CertReports.Syncfusion/Services/Implementations/FundAnagraficaRenderer.cs @@ -11,11 +11,19 @@ public class FundAnagraficaRenderer private const float PageW = 595f - 2 * PdfTheme.PageMargin; private const float PageH = 842f - 2 * PdfTheme.PageMargin - PdfTheme.FooterHeight; private const float ColGap = 8f; - private const float Col1W = 190f; - private const float Col2W = 125f; - private const float Col3W = PageW - Col1W - Col2W - 2 * ColGap; + private const float Col1W = 247f; + private const float Col2W = 78f; // ESG ridotto per dare spazio a Dati Anagrafici + private const float Col3W = PageW - Col1W - Col2W - 2 * ColGap; // = 174 - public PdfDocument Render(FundReportData data) + // Strip box widths — usati sia nello strip che per allineare titolo e Col3 + private const float StripW1 = 100f; // ISIN + private const float StripW2 = 225f; // CATEGORIA + private const float StripGap = 8f; + private const float StripTitleW = StripW1 + StripGap + StripW2; // = 333 → bordo destro titolo = bordo destro CATEGORIA + private const float StripBoxX = StripTitleW + StripGap; // = 341 → x di RANKING e box combinato + private const float StripW3 = PageW - StripBoxX; // = 174 → RANKING e box combinato fino al bordo pagina + + public PdfDocument Render(FundReportData data, byte[]? chartPng = null) { var doc = new PdfDocument(); var page = PdfTheme.AddA4Page(doc); @@ -28,13 +36,15 @@ public class FundAnagraficaRenderer y = DrawStrip(g, info, y); y += 8f; - float col1X = 0f; - float col2X = Col1W + ColGap; - float col3X = col2X + Col2W + ColGap; + float contentY = y; + float col1Bottom = DrawAnagrafici(g, info, 0f, Col1W, contentY); + float col2Bottom = DrawEsg (g, info, Col1W + ColGap, Col2W, contentY); + float col3Bottom = DrawPerfGrid (g, info, Col1W + ColGap + Col2W + ColGap, Col3W, contentY); - DrawAnagrafici(g, info, col1X, Col1W, y); - DrawEsg(g, info, col2X, Col2W, y); - DrawPerfGrid(g, info, col3X, Col3W, y); + y = Math.Max(col1Bottom, Math.Max(col2Bottom, col3Bottom)) + 14f; + + if (chartPng != null) + DrawChartImage(g, chartPng, y); PdfTheme.DrawFooter(g, PageW, PageH, 1, data.ShowBranding); return doc; @@ -42,37 +52,42 @@ public class FundAnagraficaRenderer private float DrawTitle(PdfGraphics g, FundInfo info, float y) { - const float h = 22f; - g.DrawRectangle(PdfTheme.AccentBlueBrush, new RectangleF(0, y, PageW, h)); + const float h = 36f; + + // Barra blu titolo — bordo destro allineato con CATEGORIA + g.DrawRectangle(PdfTheme.AccentBlueBrush, new RectangleF(0, y, StripTitleW, h)); var fmt = new PdfStringFormat { - Alignment = PdfTextAlignment.Center, + Alignment = PdfTextAlignment.Left, LineAlignment = PdfVerticalAlignment.Middle }; - var title = $"{info.Tipo ?? "Fondo"} — {info.Strumento} — {info.Isin}"; - g.DrawString(title, PdfTheme.Bold, + var tipo = (info.Tipo ?? "FONDO").ToUpperInvariant(); + g.DrawString($"{tipo} — {info.Strumento}", PdfTheme.SectionTitleFont, new PdfSolidBrush(new PdfColor(Color.FromArgb(255, 255, 255, 255))), - new RectangleF(0, y, PageW, h), fmt); + new RectangleF(6f, y, StripTitleW - 6f, h), fmt); + + // Box combinato PREZZO + AGGIORNATO AL — stesso x/w di RANKING + var prezzoStr = info.Prezzo.HasValue ? $"{info.Prezzo.Value:F2} {info.Valuta}" : "—"; + var dataStr = info.DataPrezzo.HasValue ? info.DataPrezzo.Value.ToString("dd/MM/yyyy") : "—"; + DrawCombinedStripBox(g, "PREZZO", prezzoStr, "AGGIORNATO AL", dataStr, + StripBoxX, y, StripW3, h); + return y + h; } private float DrawStrip(PdfGraphics g, FundInfo info, float y) { - const float h = 36f; - const float w1 = 100f; - const float w2 = 225f; - const float w3 = 150f; - const float gap = 8f; + const float h = 36f; - DrawStripBox(g, "RATING / RANK", + DrawStripBox(g, "ISIN", + info.Isin, + 0f, y, StripW1, h); + DrawStripBox(g, "CATEGORIA", + info.CategoriaMorningstar ?? "—", + StripW1 + StripGap, y, StripW2, h); + DrawStripBox(g, "RANKING", info.Rank.HasValue ? info.Rank.Value.ToString("F2") : "—", - 0f, y, w1, h); - DrawStripBox(g, "PREZZO", - info.Prezzo.HasValue ? $"{info.Prezzo.Value:F2} {info.Valuta}" : "—", - w1 + gap, y, w2, h); - DrawStripBox(g, "AGGIORNATO AL", - info.DataPrezzo.HasValue ? info.DataPrezzo.Value.ToString("dd/MM/yyyy") : "—", - w1 + gap + w2 + gap, y, w3, h); + StripBoxX, y, StripW3, h); return y + h; } @@ -82,7 +97,7 @@ public class FundAnagraficaRenderer { var bgBrush = new PdfSolidBrush(new PdfColor(Color.FromArgb(255, 232, 238, 248))); var borderPen = new PdfPen(new PdfColor(PdfTheme.AccentBlue), 0.5f); - g.DrawRectangle(bgBrush, new RectangleF(x, y, w, h)); + g.DrawRectangle(bgBrush, new RectangleF(x, y, w, h)); g.DrawRectangle(borderPen, new RectangleF(x, y, w, h)); g.DrawRectangle(PdfTheme.AccentBlueBrush, new RectangleF(x, y, 3f, h)); var grayBrush = new PdfSolidBrush(new PdfColor(Color.FromArgb(255, 136, 136, 136))); @@ -93,29 +108,57 @@ public class FundAnagraficaRenderer new RectangleF(x + 6f, y + 13f, w - 8f, h - 15f), leftFmt); } - private void DrawAnagrafici(PdfGraphics g, FundInfo info, float x, float w, float y) + // Un unico box con due coppie label/valore affiancate sulla stessa riga + private void DrawCombinedStripBox(PdfGraphics g, + string label1, string value1, string label2, string value2, + float x, float y, float w, float h) + { + var bgBrush = new PdfSolidBrush(new PdfColor(Color.FromArgb(255, 232, 238, 248))); + var borderPen = new PdfPen(new PdfColor(PdfTheme.AccentBlue), 0.5f); + g.DrawRectangle(bgBrush, new RectangleF(x, y, w, h)); + g.DrawRectangle(borderPen, new RectangleF(x, y, w, h)); + g.DrawRectangle(PdfTheme.AccentBlueBrush, new RectangleF(x, y, 3f, h)); + + var grayBrush = new PdfSolidBrush(new PdfColor(Color.FromArgb(255, 136, 136, 136))); + var leftFmt = new PdfStringFormat { Alignment = PdfTextAlignment.Left }; + float half = w / 2f; + + // Sinistra: PREZZO + g.DrawString(label1, PdfTheme.Small, grayBrush, + new RectangleF(x + 6f, y + 2f, half - 8f, 10f), leftFmt); + g.DrawString(value1, PdfTheme.Bold, PdfTheme.AccentBlueBrush, + new RectangleF(x + 6f, y + 13f, half - 8f, h - 15f), leftFmt); + + // Destra: AGGIORNATO AL + g.DrawString(label2, PdfTheme.Small, grayBrush, + new RectangleF(x + half + 2f, y + 2f, half - 6f, 10f), leftFmt); + g.DrawString(value2, PdfTheme.Bold, PdfTheme.AccentBlueBrush, + new RectangleF(x + half + 2f, y + 13f, half - 6f, h - 15f), leftFmt); + } + + private float DrawAnagrafici(PdfGraphics g, FundInfo info, float x, float w, float y) { y = DrawColHeader(g, "Dati Anagrafici", x, w, y); var items = new (string Label, string Value)[] { - ("Società", info.Societa ?? "—"), - ("Categoria MS", info.CategoriaMorningstar ?? "—"), - ("Tipo", info.Tipo ?? "—"), - ("Valuta", info.Valuta ?? "—"), + ("Società", info.Societa ?? "—"), + ("Tipo", info.Tipo?.ToUpperInvariant() ?? "—"), + ("Valuta", info.Valuta ?? "—"), ("Hedged", string.IsNullOrEmpty(info.Hedged) ? "—" : info.Hedged), - ("Benchmark", info.Benchmark ?? "—"), + ("Benchmark", info.Benchmark ?? "—"), ("Spese correnti", info.SpeseCorrenti.HasValue ? $"{info.SpeseCorrenti.Value:F2}%" : "—"), - ("Catalogo", info.Catalogo ?? "—"), - ("Proventi", info.Proventi ?? "—"), + ("Catalogo", info.Catalogo ?? "—"), + ("Proventi", info.Proventi ?? "—"), ("Data lancio", info.DataLancio.HasValue ? info.DataLancio.Value.ToString("dd/MM/yyyy") : "—"), ("Patrimonio", info.Patrimonio.HasValue ? $"{info.Patrimonio.Value:N0} EUR" : "—"), }; DrawKvList(g, items, x, w, y); + return y + items.Length * PdfTheme.RowHeight; } - private void DrawEsg(PdfGraphics g, FundInfo info, float x, float w, float y) + private float DrawEsg(PdfGraphics g, FundInfo info, float x, float w, float y) { y = DrawColHeader(g, "ESG Score", x, w, y); @@ -144,11 +187,14 @@ public class FundAnagraficaRenderer new RectangleF(x + 5f, y + 14f, w - 8f, 20f)); y += cardH + 3f; } + + return y; } - private void DrawPerfGrid(PdfGraphics g, FundInfo info, float x, float w, float y) + private float DrawPerfGrid(PdfGraphics g, FundInfo info, float x, float w, float y) { - y = DrawGridHeader(g, "PERFORMANCE · VOLATILITÀ · REND/RISK", x, w, y); + // Intestazione con barretta blu laterale (stesso stile di Dati Anagrafici e ESG) + y = DrawColHeader(g, "PERFORMANCE · VOLATILITÀ · REND/RISK", x, w, y); var periods = new (string Label, decimal? Perf, decimal? Vol, decimal? Rr)[] { @@ -161,8 +207,8 @@ public class FundAnagraficaRenderer }; const float cellGap = 3f; + const float cellH = 50f; float cellW = (w - 2 * cellGap) / 3f; - const float cellH = 50f; for (int i = 0; i < 6; i++) { @@ -173,6 +219,18 @@ public class FundAnagraficaRenderer var p = periods[i]; DrawPerfCell(g, p.Label, p.Perf, p.Vol, p.Rr, cx, cy, cellW, cellH); } + + return y + 2 * (cellH + cellGap) - cellGap; // 2 righe + } + + private void DrawChartImage(PdfGraphics g, byte[] pngBytes, float y) + { + float chartH = PageH - y - 4f; + if (chartH < 60f) return; + + using var imgStream = new MemoryStream(pngBytes); + var pdfImage = PdfImage.FromStream(imgStream); + g.DrawImage(pdfImage, new RectangleF(0f, y, PageW, chartH)); } private void DrawPerfCell(PdfGraphics g, string period, @@ -230,27 +288,12 @@ public class FundAnagraficaRenderer return y + 18f; } - private float DrawGridHeader(PdfGraphics g, string title, float x, float w, float y) - { - const float h = 16f; - g.DrawRectangle(PdfTheme.AccentBlueBrush, new RectangleF(x, y, w, h)); - var fmt = new PdfStringFormat - { - Alignment = PdfTextAlignment.Left, - LineAlignment = PdfVerticalAlignment.Middle - }; - g.DrawString(title, PdfTheme.SmallBold, - new PdfSolidBrush(new PdfColor(Color.FromArgb(255, 255, 255, 255))), - new RectangleF(x + 4f, y, w - 6f, h), fmt); - return y + h + 4f; - } - private void DrawKvList(PdfGraphics g, (string Label, string Value)[] items, float x, float w, float y) { float rh = PdfTheme.RowHeight; - float labelW = w * 0.52f; - float valueW = w * 0.48f; + float labelW = w * 0.30f; + float valueW = w * 0.70f; var labelBrush = new PdfSolidBrush(new PdfColor(Color.FromArgb(255, 85, 85, 85))); var valueBrush = new PdfSolidBrush(new PdfColor(Color.FromArgb(255, 33, 33, 33))); diff --git a/CertReports.Syncfusion/Services/Implementations/FundChartSectionRenderer.cs b/CertReports.Syncfusion/Services/Implementations/FundChartSectionRenderer.cs index fb0baea..b9af517 100644 --- a/CertReports.Syncfusion/Services/Implementations/FundChartSectionRenderer.cs +++ b/CertReports.Syncfusion/Services/Implementations/FundChartSectionRenderer.cs @@ -17,14 +17,12 @@ public class FundChartSectionRenderer _logger = logger; } - public async Task RenderAsync(string isin, string instrumentName) + public async Task GetChartPngAsync(string isin, string instrumentName) { try { - var points = await _dataService.GetChartPricesAsync(isin); - var pngBytes = FundSkiaChartRenderer.Render(points, instrumentName, - width: 1100, height: 650); - return WrapInPdf(pngBytes); + var points = await _dataService.GetChartPricesAsync(isin); + return FundSkiaChartRenderer.Render(points, instrumentName, width: 1100, height: 550); } catch (Exception ex) { @@ -33,6 +31,12 @@ public class FundChartSectionRenderer } } + public async Task RenderAsync(string isin, string instrumentName) + { + var pngBytes = await GetChartPngAsync(isin, instrumentName); + return pngBytes != null ? WrapInPdf(pngBytes) : null; + } + private static PdfDocument WrapInPdf(byte[] pngBytes) { var doc = new PdfDocument(); diff --git a/CertReports.Syncfusion/Services/Implementations/FundReportOrchestrator.cs b/CertReports.Syncfusion/Services/Implementations/FundReportOrchestrator.cs index 6374b6a..b7cbae9 100644 --- a/CertReports.Syncfusion/Services/Implementations/FundReportOrchestrator.cs +++ b/CertReports.Syncfusion/Services/Implementations/FundReportOrchestrator.cs @@ -46,23 +46,17 @@ public class FundReportOrchestrator : IFundReportOrchestrator throw new InvalidOperationException($"Nessun dato trovato per ISIN {isin}"); var reportData = new FundReportData { Info = info, ShowBranding = showBranding }; - var sections = new List(); - sections.Add(_anagraficaRenderer.Render(reportData)); + var chartPng = await _chartRenderer.GetChartPngAsync(isin, info.Strumento); + if (chartPng != null) + _logger.LogInformation("FundChart generato per {Isin}", isin); + else + _logger.LogWarning("FundChart omesso per {Isin} (dati insufficienti o errore)", isin); + + var doc = _anagraficaRenderer.Render(reportData, chartPng); _logger.LogInformation("Sezione 'FundAnagrafica' generata per {Isin}", isin); - var chartDoc = await _chartRenderer.RenderAsync(isin, info.Strumento); - if (chartDoc != null) - { - sections.Add(chartDoc); - _logger.LogInformation("Sezione 'FundChart' generata per {Isin}", isin); - } - else - { - _logger.LogWarning("Sezione 'FundChart' omessa per {Isin} (dati insufficienti o errore)", isin); - } - - var finalPdf = _merger.Merge(sections); + var finalPdf = _merger.Merge(new List { doc }); _cache.Set(cacheKey, finalPdf); _logger.LogInformation("Fund report generato per {Isin}: {Size} bytes", isin, finalPdf.Length); return finalPdf; diff --git a/CertReports.Syncfusion/Services/Implementations/SkiaChartRendererV2.cs b/CertReports.Syncfusion/Services/Implementations/SkiaChartRendererV2.cs index 447bbf3..38d5276 100644 --- a/CertReports.Syncfusion/Services/Implementations/SkiaChartRendererV2.cs +++ b/CertReports.Syncfusion/Services/Implementations/SkiaChartRendererV2.cs @@ -160,7 +160,9 @@ public static class SkiaChartRendererV2 } DrawSeriesV2(canvas, plotArea, ulPoints, minDate, maxDate, minY, maxY, color, thickness); - DrawSeriesEndLabel(plotArea, ulPoints, minDate, maxDate, minY, maxY, color, endLabel, rightLabels); + // Il WorstOf ha già il label sul margine destro dalla linea tratteggiata — evita duplicato + if (ul.IsWorstOf != 1) + DrawSeriesEndLabel(plotArea, ulPoints, minDate, maxDate, minY, maxY, color, endLabel, rightLabels); seriesLegend.Add((seriesLabel, color, false, thickness)); }