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