fix: fund report layout refactor and chart renderer fix
- FundAnagraficaRenderer: unified header style (DrawColHeader), DrawPerfGrid returns float, chart embedded in anagrafica section
- FundReportController: simplify filename to {isin}.pdf
- SkiaChartRendererV2: minor fix affecting certificate chart
Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -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"))
|
||||
|
||||
@@ -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;
|
||||
|
||||
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;
|
||||
}
|
||||
@@ -93,15 +108,42 @@ 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 ?? "—"),
|
||||
("Tipo", info.Tipo?.ToUpperInvariant() ?? "—"),
|
||||
("Valuta", info.Valuta ?? "—"),
|
||||
("Hedged", string.IsNullOrEmpty(info.Hedged) ? "—" : info.Hedged),
|
||||
("Benchmark", info.Benchmark ?? "—"),
|
||||
@@ -113,9 +155,10 @@ public class FundAnagraficaRenderer
|
||||
};
|
||||
|
||||
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;
|
||||
float cellW = (w - 2 * cellGap) / 3f;
|
||||
const float cellH = 50f;
|
||||
float cellW = (w - 2 * cellGap) / 3f;
|
||||
|
||||
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)));
|
||||
|
||||
|
||||
@@ -17,14 +17,12 @@ public class FundChartSectionRenderer
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
public async Task<PdfDocument?> RenderAsync(string isin, string instrumentName)
|
||||
public async Task<byte[]?> 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);
|
||||
return FundSkiaChartRenderer.Render(points, instrumentName, width: 1100, height: 550);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
@@ -33,6 +31,12 @@ public class FundChartSectionRenderer
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<PdfDocument?> 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();
|
||||
|
||||
@@ -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<PdfDocument>();
|
||||
|
||||
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<PdfDocument> { doc });
|
||||
_cache.Set(cacheKey, finalPdf);
|
||||
_logger.LogInformation("Fund report generato per {Isin}: {Size} bytes", isin, finalPdf.Length);
|
||||
return finalPdf;
|
||||
|
||||
@@ -160,6 +160,8 @@ public static class SkiaChartRendererV2
|
||||
}
|
||||
|
||||
DrawSeriesV2(canvas, plotArea, ulPoints, minDate, maxDate, minY, maxY, color, thickness);
|
||||
// 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));
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user