This commit is contained in:
2025-11-28 11:09:30 +01:00
parent 14b3e965d0
commit bb22213d19
4 changed files with 103 additions and 23 deletions

View File

@@ -4,14 +4,62 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co
--- ---
## ISTRUZIONI OBBLIGATORIE PER CLAUDE
### Auto-Aggiornamento CLAUDE.md
**OBBLIGATORIO:** Claude DEVE aggiornare questo file CLAUDE.md ogni volta che:
1. **Viene completato un task significativo** (fix, nuova feature, refactoring importante)
2. **Viene risolto un problema tecnico** che potrebbe ripresentarsi in futuro
3. **Si scopre un pattern/workaround** importante da ricordare
4. **Termina una sessione di lavoro** - aggiornare "Quick Start - Session Recovery"
**Cosa aggiornare:**
- **Sezione "Quick Start - Session Recovery":**
- Aggiornare "Ultima sessione" con data corrente
- Spostare lavoro completato da "ultima sessione" a "sessioni precedenti"
- Aggiungere nuovi task completati alla lista
- Aggiornare "Prossimi task prioritari" (spuntare completati, aggiungere nuovi)
- **Sezione "Problemi Risolti (da ricordare)":**
- Aggiungere OGNI problema tecnico risolto con:
- Descrizione del problema
- Causa root
- Soluzione implementata
- File coinvolti
- **Checklist:**
- Aggiornare checkbox `[x]` per task completati
- Aggiungere nuovi task se scoperti durante il lavoro
**Formato per nuovi problemi risolti:**
```markdown
XX. **Nome Problema (FIX/IMPLEMENTATO DATA):** - **Problema:** Descrizione breve - **Causa:** Perché succedeva - **Soluzione:** Come è stato risolto - **File:** File modificati
```
**NON dimenticare:** Questo file è la memoria persistente tra sessioni. Se non viene aggiornato, il lavoro fatto andrà perso e dovrà essere riscoperto.
---
## Quick Start - Session Recovery ## Quick Start - Session Recovery
**Ultima sessione:** 28 Novembre 2025 **Ultima sessione:** 28 Novembre 2025 (sera)
**Stato progetto:** Migrazione Oracle APEX → .NET + React TypeScript in corso **Stato progetto:** Migrazione Oracle APEX → .NET + React TypeScript in corso
**Lavoro completato nell'ultima sessione:** **Lavoro completato nell'ultima sessione:**
- **FIX: Variabili Globali Report ({{$pageNumber}}, {{$totalPages}}, ecc.)** - RISOLTO
- Le variabili speciali ora vengono correttamente risolte nel PDF finale
- Aggiunta classe `PageContext` per passare numero pagina e totale pagine durante il rendering
- Propagato `PageContext` attraverso tutta la catena di rendering (bitmap, text, binding resolution)
- `ResolveBindingPath()` ora restituisce valori reali invece di placeholder
**Lavoro completato nelle sessioni precedenti (28 Novembre 2025):**
- **NUOVA FEATURE: Gestione Multi-Pagina nel Report Designer** - Completata - **NUOVA FEATURE: Gestione Multi-Pagina nel Report Designer** - Completata
- Nuovo tipo `AprtPage` per definire pagine con impostazioni individuali (size, orientation, margins, backgroundColor) - Nuovo tipo `AprtPage` per definire pagine con impostazioni individuali (size, orientation, margins, backgroundColor)
- Ogni elemento ha `pageId` per assegnazione a pagina specifica - Ogni elemento ha `pageId` per assegnazione a pagina specifica
@@ -878,6 +926,23 @@ frontend/src/
- **Migrazione template legacy:** `MigrateTemplatePages()` crea pagina default e assegna elementi orfani - **Migrazione template legacy:** `MigrateTemplatePages()` crea pagina default e assegna elementi orfani
15. **Variabili Globali Report (FIX 28/11/2025 sera):**
- **Problema:** Le variabili speciali `{{$pageNumber}}`, `{{$totalPages}}`, `{{$date}}`, `{{$datetime}}`, `{{$time}}` non venivano stampate nel PDF - restavano come placeholder
- **Causa:** `ResolveBindingPath()` restituiva placeholder statici (`"{{PAGE}}"`) invece dei valori reali perché il contesto pagina non veniva passato durante il rendering
- **Soluzione:**
1. Aggiunta classe `PageContext` con `PageNumber` e `TotalPages`
2. Il ciclo di rendering ora traccia l'indice pagina corrente
3. `PageContext` propagato attraverso tutta la catena: `GeneratePdfAsync` → `RenderContentToBitmap` → `RenderElementToCanvas` → `RenderTextToCanvas` → `ResolveContent` → `ResolveBindingWithFormat` → `ResolveBindingPath`
4. `ResolveBindingPath()` ora usa i valori reali dal contesto:
```csharp
"$pageNumber" => pageContext?.PageNumber.ToString() ?? "1",
"$totalPages" => pageContext?.TotalPages.ToString() ?? "1",
"$date" => DateTime.Now.ToString("dd/MM/yyyy"),
"$time" => DateTime.Now.ToString("HH:mm"),
"$datetime" => DateTime.Now.ToString("dd/MM/yyyy HH:mm"),
```
- **File:** `ReportGeneratorService.cs` - Metodi `GeneratePdfAsync()`, `RenderContentToBitmap()`, `RenderElementToCanvas()`, `RenderTextToCanvas()`, `ResolveContent()`, `ResolveBindingWithFormat()`, `ResolveBinding()`, `ResolveExpression()`, `ResolveBindingPath()`
### Schema Database Report System ### Schema Database Report System
Le tabelle sono già nel DbContext (`AppollinareDbContext.cs`): Le tabelle sono già nel DbContext (`AppollinareDbContext.cs`):

View File

@@ -57,12 +57,16 @@ public class ReportGeneratorService
_logger.LogInformation("Elements pageIds: {PageIds}", string.Join(", ", aprt.Elements.Select(e => $"{e.Name ?? e.Id}->'{e.PageId ?? "NULL"}'"))); _logger.LogInformation("Elements pageIds: {PageIds}", string.Join(", ", aprt.Elements.Select(e => $"{e.Name ?? e.Id}->'{e.PageId ?? "NULL"}'")));
const float dpi = 300f; const float dpi = 300f;
var totalPages = aprt.Pages.Count;
// Pre-render all pages to avoid closure issues in QuestPDF lambda // Pre-render all pages to avoid closure issues in QuestPDF lambda
var pageRenderData = new List<(float pageWidth, float pageHeight, string? bgColor, byte[] imageBytes)>(); var pageRenderData = new List<(float pageWidth, float pageHeight, string? bgColor, byte[] imageBytes)>();
foreach (var pageDefinition in aprt.Pages) for (int pageIndex = 0; pageIndex < aprt.Pages.Count; pageIndex++)
{ {
var pageDefinition = aprt.Pages[pageIndex];
var currentPageNumber = pageIndex + 1;
// Determine page settings (use page override or template default) // Determine page settings (use page override or template default)
var pageSize = pageDefinition.PageSize ?? aprt.Meta.PageSize; var pageSize = pageDefinition.PageSize ?? aprt.Meta.PageSize;
var orientation = pageDefinition.Orientation ?? aprt.Meta.Orientation; var orientation = pageDefinition.Orientation ?? aprt.Meta.Orientation;
@@ -76,16 +80,18 @@ public class ReportGeneratorService
(string.IsNullOrEmpty(e.PageId) && pageDefinition.Id == aprt.Pages[0].Id))) (string.IsNullOrEmpty(e.PageId) && pageDefinition.Id == aprt.Pages[0].Id)))
.ToList(); .ToList();
_logger.LogInformation("Page '{PageName}' ({PageId}): {ElementCount} elements (of {TotalElements} total), Size: {Width}x{Height}mm", _logger.LogInformation("Page '{PageName}' ({PageId}): {ElementCount} elements (of {TotalElements} total), Size: {Width}x{Height}mm, Page {CurrentPage}/{TotalPages}",
pageDefinition.Name, pageDefinition.Id, pageElements.Count, aprt.Elements.Count, pageDefinition.Name, pageDefinition.Id, pageElements.Count, aprt.Elements.Count,
pageWidth, pageHeight); pageWidth, pageHeight, currentPageNumber, totalPages);
_logger.LogInformation(" Elements on this page: {Elements}", _logger.LogInformation(" Elements on this page: {Elements}",
string.Join(", ", pageElements.Select(e => e.Name ?? e.Id))); string.Join(", ", pageElements.Select(e => e.Name ?? e.Id)));
// Render full page to bitmap (element coordinates are relative to entire page) // Render full page to bitmap (element coordinates are relative to entire page)
// Pass page context for special variables
var pageContext = new PageContext { PageNumber = currentPageNumber, TotalPages = totalPages };
var pageImageBytes = RenderContentToBitmap( var pageImageBytes = RenderContentToBitmap(
pageElements, pageWidth, pageHeight, dataContext, resources, dpi, pageElements, pageWidth, pageHeight, dataContext, resources, dpi,
pageDefinition.BackgroundColor); pageDefinition.BackgroundColor, pageContext);
pageRenderData.Add((pageWidth, pageHeight, pageDefinition.BackgroundColor, pageImageBytes)); pageRenderData.Add((pageWidth, pageHeight, pageDefinition.BackgroundColor, pageImageBytes));
} }
@@ -112,6 +118,15 @@ public class ReportGeneratorService
return document.GeneratePdf(); return document.GeneratePdf();
} }
/// <summary>
/// Context for page-level special variables during rendering
/// </summary>
private class PageContext
{
public int PageNumber { get; set; }
public int TotalPages { get; set; }
}
/// <summary> /// <summary>
/// Migrates legacy templates without pages to the new multi-page format. /// Migrates legacy templates without pages to the new multi-page format.
/// Creates a default page and assigns all orphan elements to it. /// Creates a default page and assigns all orphan elements to it.
@@ -141,7 +156,7 @@ public class ReportGeneratorService
/// </summary> /// </summary>
private byte[] RenderContentToBitmap(List<AprtElement> elements, float contentWidthMm, float contentHeightMm, private byte[] RenderContentToBitmap(List<AprtElement> elements, float contentWidthMm, float contentHeightMm,
Dictionary<string, object> dataContext, Dictionary<string, object> resources, float dpi, Dictionary<string, object> dataContext, Dictionary<string, object> resources, float dpi,
string? backgroundColor = null) string? backgroundColor = null, PageContext? pageContext = null)
{ {
// Use a SINGLE uniform scale for both axes - critical for correct element positioning // Use a SINGLE uniform scale for both axes - critical for correct element positioning
// With FitArea(), the bitmap will fill the page maintaining proportions // With FitArea(), the bitmap will fill the page maintaining proportions
@@ -167,7 +182,7 @@ public class ReportGeneratorService
// Render each element using consistent scale factors // Render each element using consistent scale factors
foreach (var element in elements.OrderBy(e => elements.IndexOf(e))) foreach (var element in elements.OrderBy(e => elements.IndexOf(e)))
{ {
RenderElementToCanvas(canvas, element, scaleX, scaleY, dataContext, resources); RenderElementToCanvas(canvas, element, scaleX, scaleY, dataContext, resources, pageContext);
} }
// Encode to PNG // Encode to PNG
@@ -192,7 +207,7 @@ public class ReportGeneratorService
/// 3. Draw the element centered on this point (offset by -width/2, -height/2) /// 3. Draw the element centered on this point (offset by -width/2, -height/2)
/// </summary> /// </summary>
private void RenderElementToCanvas(SKCanvas canvas, AprtElement element, float scaleX, float scaleY, private void RenderElementToCanvas(SKCanvas canvas, AprtElement element, float scaleX, float scaleY,
Dictionary<string, object> dataContext, Dictionary<string, object> resources) Dictionary<string, object> dataContext, Dictionary<string, object> resources, PageContext? pageContext = null)
{ {
// Convert mm to pixels // Convert mm to pixels
var left = element.Position.X * scaleX; var left = element.Position.X * scaleX;
@@ -244,7 +259,7 @@ public class ReportGeneratorService
break; break;
case "text": case "text":
RenderTextToCanvas(canvas, element, drawX, drawY, width, height, scaleX, scaleY, dataContext); RenderTextToCanvas(canvas, element, drawX, drawY, width, height, scaleX, scaleY, dataContext, pageContext);
break; break;
case "image": case "image":
@@ -308,10 +323,10 @@ public class ReportGeneratorService
} }
private void RenderTextToCanvas(SKCanvas canvas, AprtElement element, float x, float y, float width, float height, private void RenderTextToCanvas(SKCanvas canvas, AprtElement element, float x, float y, float width, float height,
float scaleX, float scaleY, Dictionary<string, object> dataContext) float scaleX, float scaleY, Dictionary<string, object> dataContext, PageContext? pageContext = null)
{ {
var style = element.Style; var style = element.Style;
var text = ResolveContent(element.Content, dataContext); var text = ResolveContent(element.Content, dataContext, pageContext);
if (string.IsNullOrEmpty(text)) return; if (string.IsNullOrEmpty(text)) return;
// Use average scale for font size to maintain proportions // Use average scale for font size to maintain proportions
@@ -1478,27 +1493,27 @@ public class ReportGeneratorService
return null; return null;
} }
private string ResolveContent(AprtContent? content, Dictionary<string, object> dataContext) private string ResolveContent(AprtContent? content, Dictionary<string, object> dataContext, PageContext? pageContext = null)
{ {
if (content == null) return string.Empty; if (content == null) return string.Empty;
var result = content.Type?.ToLower() switch var result = content.Type?.ToLower() switch
{ {
"static" => content.Value ?? string.Empty, "static" => content.Value ?? string.Empty,
"binding" => ResolveBindingWithFormat(content.Expression ?? content.Value ?? string.Empty, dataContext, content.Format), "binding" => ResolveBindingWithFormat(content.Expression ?? content.Value ?? string.Empty, dataContext, content.Format, pageContext),
"expression" => ResolveExpression(content.Value ?? string.Empty, dataContext), "expression" => ResolveExpression(content.Value ?? string.Empty, dataContext, pageContext),
_ => content.Value ?? string.Empty _ => content.Value ?? string.Empty
}; };
return result; return result;
} }
private string ResolveBindingWithFormat(string expression, Dictionary<string, object> dataContext, string? format) private string ResolveBindingWithFormat(string expression, Dictionary<string, object> dataContext, string? format, PageContext? pageContext = null)
{ {
return BindingRegex.Replace(expression, match => return BindingRegex.Replace(expression, match =>
{ {
var path = match.Groups[1].Value.Trim(); var path = match.Groups[1].Value.Trim();
var value = ResolveBindingPath(path, dataContext); var value = ResolveBindingPath(path, dataContext, pageContext);
if (value != null && !string.IsNullOrEmpty(format)) if (value != null && !string.IsNullOrEmpty(format))
{ {
@@ -1509,25 +1524,25 @@ public class ReportGeneratorService
}); });
} }
private string ResolveBinding(string expression, Dictionary<string, object> dataContext) private string ResolveBinding(string expression, Dictionary<string, object> dataContext, PageContext? pageContext = null)
{ {
return BindingRegex.Replace(expression, match => return BindingRegex.Replace(expression, match =>
{ {
var path = match.Groups[1].Value.Trim(); var path = match.Groups[1].Value.Trim();
var value = ResolveBindingPath(path, dataContext); var value = ResolveBindingPath(path, dataContext, pageContext);
return value?.ToString() ?? string.Empty; return value?.ToString() ?? string.Empty;
}); });
} }
private object? ResolveBindingPath(string path, Dictionary<string, object> dataContext) private object? ResolveBindingPath(string path, Dictionary<string, object> dataContext, PageContext? pageContext = null)
{ {
// Handle special variables // Handle special variables
if (path.StartsWith("$")) if (path.StartsWith("$"))
{ {
return path switch return path switch
{ {
"$pageNumber" => "{{PAGE}}", "$pageNumber" => pageContext?.PageNumber.ToString() ?? "1",
"$totalPages" => "{{TOTALPAGES}}", "$totalPages" => pageContext?.TotalPages.ToString() ?? "1",
"$date" => DateTime.Now.ToString("dd/MM/yyyy"), "$date" => DateTime.Now.ToString("dd/MM/yyyy"),
"$time" => DateTime.Now.ToString("HH:mm"), "$time" => DateTime.Now.ToString("HH:mm"),
"$datetime" => DateTime.Now.ToString("dd/MM/yyyy HH:mm"), "$datetime" => DateTime.Now.ToString("dd/MM/yyyy HH:mm"),
@@ -1566,9 +1581,9 @@ public class ReportGeneratorService
return current; return current;
} }
private string ResolveExpression(string expression, Dictionary<string, object> dataContext) private string ResolveExpression(string expression, Dictionary<string, object> dataContext, PageContext? pageContext = null)
{ {
return ResolveBinding(expression, dataContext); return ResolveBinding(expression, dataContext, pageContext);
} }
private object? GetPropertyValue(object obj, string propertyPath) private object? GetPropertyValue(object obj, string propertyPath)

Binary file not shown.

Binary file not shown.