diff --git a/CLAUDE.md b/CLAUDE.md
index be44fdd..a1d43e4 100644
--- a/CLAUDE.md
+++ b/CLAUDE.md
@@ -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
-**Ultima sessione:** 28 Novembre 2025
+**Ultima sessione:** 28 Novembre 2025 (sera)
**Stato progetto:** Migrazione Oracle APEX → .NET + React TypeScript in corso
**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
- Nuovo tipo `AprtPage` per definire pagine con impostazioni individuali (size, orientation, margins, backgroundColor)
- 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
+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
Le tabelle sono già nel DbContext (`AppollinareDbContext.cs`):
diff --git a/src/Apollinare.API/Services/Reports/ReportGeneratorService.cs b/src/Apollinare.API/Services/Reports/ReportGeneratorService.cs
index ed37816..71f8e76 100644
--- a/src/Apollinare.API/Services/Reports/ReportGeneratorService.cs
+++ b/src/Apollinare.API/Services/Reports/ReportGeneratorService.cs
@@ -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"}'")));
const float dpi = 300f;
+ var totalPages = aprt.Pages.Count;
// Pre-render all pages to avoid closure issues in QuestPDF lambda
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)
var pageSize = pageDefinition.PageSize ?? aprt.Meta.PageSize;
var orientation = pageDefinition.Orientation ?? aprt.Meta.Orientation;
@@ -76,16 +80,18 @@ public class ReportGeneratorService
(string.IsNullOrEmpty(e.PageId) && pageDefinition.Id == aprt.Pages[0].Id)))
.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,
- pageWidth, pageHeight);
+ pageWidth, pageHeight, currentPageNumber, totalPages);
_logger.LogInformation(" Elements on this page: {Elements}",
string.Join(", ", pageElements.Select(e => e.Name ?? e.Id)));
// 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(
pageElements, pageWidth, pageHeight, dataContext, resources, dpi,
- pageDefinition.BackgroundColor);
+ pageDefinition.BackgroundColor, pageContext);
pageRenderData.Add((pageWidth, pageHeight, pageDefinition.BackgroundColor, pageImageBytes));
}
@@ -112,6 +118,15 @@ public class ReportGeneratorService
return document.GeneratePdf();
}
+ ///
+ /// Context for page-level special variables during rendering
+ ///
+ private class PageContext
+ {
+ public int PageNumber { get; set; }
+ public int TotalPages { get; set; }
+ }
+
///
/// Migrates legacy templates without pages to the new multi-page format.
/// Creates a default page and assigns all orphan elements to it.
@@ -141,7 +156,7 @@ public class ReportGeneratorService
///
private byte[] RenderContentToBitmap(List elements, float contentWidthMm, float contentHeightMm,
Dictionary dataContext, Dictionary 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
// With FitArea(), the bitmap will fill the page maintaining proportions
@@ -167,7 +182,7 @@ public class ReportGeneratorService
// Render each element using consistent scale factors
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
@@ -192,7 +207,7 @@ public class ReportGeneratorService
/// 3. Draw the element centered on this point (offset by -width/2, -height/2)
///
private void RenderElementToCanvas(SKCanvas canvas, AprtElement element, float scaleX, float scaleY,
- Dictionary dataContext, Dictionary resources)
+ Dictionary dataContext, Dictionary resources, PageContext? pageContext = null)
{
// Convert mm to pixels
var left = element.Position.X * scaleX;
@@ -244,7 +259,7 @@ public class ReportGeneratorService
break;
case "text":
- RenderTextToCanvas(canvas, element, drawX, drawY, width, height, scaleX, scaleY, dataContext);
+ RenderTextToCanvas(canvas, element, drawX, drawY, width, height, scaleX, scaleY, dataContext, pageContext);
break;
case "image":
@@ -308,10 +323,10 @@ public class ReportGeneratorService
}
private void RenderTextToCanvas(SKCanvas canvas, AprtElement element, float x, float y, float width, float height,
- float scaleX, float scaleY, Dictionary dataContext)
+ float scaleX, float scaleY, Dictionary dataContext, PageContext? pageContext = null)
{
var style = element.Style;
- var text = ResolveContent(element.Content, dataContext);
+ var text = ResolveContent(element.Content, dataContext, pageContext);
if (string.IsNullOrEmpty(text)) return;
// Use average scale for font size to maintain proportions
@@ -1478,27 +1493,27 @@ public class ReportGeneratorService
return null;
}
- private string ResolveContent(AprtContent? content, Dictionary dataContext)
+ private string ResolveContent(AprtContent? content, Dictionary dataContext, PageContext? pageContext = null)
{
if (content == null) return string.Empty;
var result = content.Type?.ToLower() switch
{
"static" => content.Value ?? string.Empty,
- "binding" => ResolveBindingWithFormat(content.Expression ?? content.Value ?? string.Empty, dataContext, content.Format),
- "expression" => ResolveExpression(content.Value ?? string.Empty, dataContext),
+ "binding" => ResolveBindingWithFormat(content.Expression ?? content.Value ?? string.Empty, dataContext, content.Format, pageContext),
+ "expression" => ResolveExpression(content.Value ?? string.Empty, dataContext, pageContext),
_ => content.Value ?? string.Empty
};
return result;
}
- private string ResolveBindingWithFormat(string expression, Dictionary dataContext, string? format)
+ private string ResolveBindingWithFormat(string expression, Dictionary dataContext, string? format, PageContext? pageContext = null)
{
return BindingRegex.Replace(expression, match =>
{
var path = match.Groups[1].Value.Trim();
- var value = ResolveBindingPath(path, dataContext);
+ var value = ResolveBindingPath(path, dataContext, pageContext);
if (value != null && !string.IsNullOrEmpty(format))
{
@@ -1509,25 +1524,25 @@ public class ReportGeneratorService
});
}
- private string ResolveBinding(string expression, Dictionary dataContext)
+ private string ResolveBinding(string expression, Dictionary dataContext, PageContext? pageContext = null)
{
return BindingRegex.Replace(expression, match =>
{
var path = match.Groups[1].Value.Trim();
- var value = ResolveBindingPath(path, dataContext);
+ var value = ResolveBindingPath(path, dataContext, pageContext);
return value?.ToString() ?? string.Empty;
});
}
- private object? ResolveBindingPath(string path, Dictionary dataContext)
+ private object? ResolveBindingPath(string path, Dictionary dataContext, PageContext? pageContext = null)
{
// Handle special variables
if (path.StartsWith("$"))
{
return path switch
{
- "$pageNumber" => "{{PAGE}}",
- "$totalPages" => "{{TOTALPAGES}}",
+ "$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"),
@@ -1566,9 +1581,9 @@ public class ReportGeneratorService
return current;
}
- private string ResolveExpression(string expression, Dictionary dataContext)
+ private string ResolveExpression(string expression, Dictionary dataContext, PageContext? pageContext = null)
{
- return ResolveBinding(expression, dataContext);
+ return ResolveBinding(expression, dataContext, pageContext);
}
private object? GetPropertyValue(object obj, string propertyPath)
diff --git a/src/Apollinare.API/apollinare.db-shm b/src/Apollinare.API/apollinare.db-shm
new file mode 100644
index 0000000..b16d5fc
Binary files /dev/null and b/src/Apollinare.API/apollinare.db-shm differ
diff --git a/src/Apollinare.API/apollinare.db-wal b/src/Apollinare.API/apollinare.db-wal
new file mode 100644
index 0000000..b6c8af8
Binary files /dev/null and b/src/Apollinare.API/apollinare.db-wal differ