diff --git a/backend/pdfrender/continuation_integration_test.go b/backend/pdfrender/continuation_integration_test.go index ec44252..c3d380b 100644 --- a/backend/pdfrender/continuation_integration_test.go +++ b/backend/pdfrender/continuation_integration_test.go @@ -65,7 +65,7 @@ func TestIntegration_ContinuationPages_ActualFiles(t *testing.T) { templateSet := DefaultA4QuerTemplateSet() var totalCap int for _, tmpl := range templateSet.Templates { - if tmpl.Metadata.Name == "page1_stats.html" { + if tmpl.Metadata.Name == "page_1.html" { for _, block := range tmpl.Metadata.Blocks { if block.ListType == "skills" { totalCap += block.MaxItems @@ -81,7 +81,7 @@ func TestIntegration_ContinuationPages_ActualFiles(t *testing.T) { // Act - Render page1 with continuations pdfs, err := RenderPageWithContinuations( viewModel, - "page1_stats.html", + "page_1.html", 1, "20.12.2025", loader, diff --git a/backend/pdfrender/continuation_test.go b/backend/pdfrender/continuation_test.go index 19226d2..df79fb4 100644 --- a/backend/pdfrender/continuation_test.go +++ b/backend/pdfrender/continuation_test.go @@ -22,7 +22,7 @@ func TestContinuationPages_WhenSkillsExceedCapacity(t *testing.T) { } // Act - Paginate skills for page1_stats - pages, err := paginator.PaginateSkills(skills, "page1_stats.html", "") + pages, err := paginator.PaginateSkills(skills, "page_1.html", "") // Assert if err != nil { @@ -34,13 +34,13 @@ func TestContinuationPages_WhenSkillsExceedCapacity(t *testing.T) { t.Errorf("Expected at least 2 pages for 100 skills, got %d", len(pages)) } - // First page should be "page1_stats.html" - if pages[0].TemplateName != "page1_stats.html" { - t.Errorf("Expected first page template 'page1_stats.html', got '%s'", pages[0].TemplateName) + // First page should be "page_1.html" + if pages[0].TemplateName != "page_1.html" { + t.Errorf("Expected first page template 'page_1.html', got '%s'", pages[0].TemplateName) } - // Second page should be continuation page with name pattern "page1.2_stats.html" - expectedContinuation := "page1.2_stats.html" + // Second page should be continuation page with name pattern "page_1.2.html" + expectedContinuation := "page_1.2.html" if pages[1].TemplateName != expectedContinuation { t.Errorf("Expected continuation page template '%s', got '%s'", expectedContinuation, pages[1].TemplateName) @@ -79,7 +79,7 @@ func TestContinuationPages_WhenWeaponsExceedCapacity(t *testing.T) { // Get capacity for page2_play weapons (should be 12) var weaponsCapacity int for _, tmpl := range templateSet.Templates { - if tmpl.Metadata.Name == "page2_play.html" { + if tmpl.Metadata.Name == "page_2.html" { for _, block := range tmpl.Metadata.Blocks { if block.ListType == "weapons" { weaponsCapacity = block.MaxItems @@ -106,7 +106,7 @@ func TestContinuationPages_WhenWeaponsExceedCapacity(t *testing.T) { t.Logf("Created %d weapons (capacity %d)", numWeapons, weaponsCapacity) // Act - pages, err := paginator.PaginateWeapons(weapons, "page2_play.html") + pages, err := paginator.PaginateWeapons(weapons, "page_2.html") // Assert if err != nil { @@ -119,13 +119,13 @@ func TestContinuationPages_WhenWeaponsExceedCapacity(t *testing.T) { } // First page should be original template - if pages[0].TemplateName != "page2_play.html" { - t.Errorf("Expected first page 'page2_play.html', got '%s'", pages[0].TemplateName) + if pages[0].TemplateName != "page_2.html" { + t.Errorf("Expected first page 'page_2.html', got '%s'", pages[0].TemplateName) } // Second page should be continuation - if pages[1].TemplateName != "page2.2_play.html" { - t.Errorf("Expected continuation 'page2.2_play.html', got '%s'", pages[1].TemplateName) + if pages[1].TemplateName != "page_2.2.html" { + t.Errorf("Expected continuation 'page_2.2.html', got '%s'", pages[1].TemplateName) } } @@ -138,7 +138,7 @@ func TestContinuationPages_MultipleOverflows(t *testing.T) { // Get actual capacity from template var skillsCapacity int for _, tmpl := range templateSet.Templates { - if tmpl.Metadata.Name == "page1_stats.html" { + if tmpl.Metadata.Name == "page_1.html" { for _, block := range tmpl.Metadata.Blocks { if block.ListType == "skills" { skillsCapacity += block.MaxItems @@ -163,7 +163,7 @@ func TestContinuationPages_MultipleOverflows(t *testing.T) { expectedPages := (200 + skillsCapacity - 1) / skillsCapacity // Act - pages, err := paginator.PaginateSkills(skills, "page1_stats.html", "") + pages, err := paginator.PaginateSkills(skills, "page_1.html", "") // Assert if err != nil { @@ -177,14 +177,14 @@ func TestContinuationPages_MultipleOverflows(t *testing.T) { t.Logf("Created %d pages for 200 skills", len(pages)) - // Verify template names follow pattern: page1_stats.html, then all use page1.2_stats.html + // Verify template names follow pattern: page_1.html, then all use page_1.2.html for i, page := range pages { var expectedTemplate string if i == 0 { - expectedTemplate = "page1_stats.html" + expectedTemplate = "page_1.html" } else { // All continuation pages use the same .2 template - expectedTemplate = "page1.2_stats.html" + expectedTemplate = "page_1.2.html" } if page.TemplateName != expectedTemplate { @@ -211,7 +211,7 @@ func TestContinuationPages_NoOverflow(t *testing.T) { } // Act - pages, err := paginator.PaginateSkills(skills, "page1_stats.html", "") + pages, err := paginator.PaginateSkills(skills, "page_1.html", "") // Assert if err != nil { @@ -224,7 +224,7 @@ func TestContinuationPages_NoOverflow(t *testing.T) { } // Should use original template, not continuation - if pages[0].TemplateName != "page1_stats.html" { - t.Errorf("Expected original template 'page1_stats.html', got '%s'", pages[0].TemplateName) + if pages[0].TemplateName != "page_1.html" { + t.Errorf("Expected original template 'page_1.html', got '%s'", pages[0].TemplateName) } } diff --git a/backend/pdfrender/fill_capacity_test.go b/backend/pdfrender/fill_capacity_test.go index 7bf13b8..a424e35 100644 --- a/backend/pdfrender/fill_capacity_test.go +++ b/backend/pdfrender/fill_capacity_test.go @@ -76,7 +76,7 @@ func TestTemplateWithEmptyRows(t *testing.T) { }, } - html, err := loader.RenderTemplate("page1_stats.html", pageData) + html, err := loader.RenderTemplate("page_1.html", pageData) if err != nil { t.Fatalf("Failed to render template: %v", err) } @@ -85,13 +85,13 @@ func TestTemplateWithEmptyRows(t *testing.T) { templateSet := DefaultA4QuerTemplateSet() var page1Template *TemplateWithMeta for i := range templateSet.Templates { - if templateSet.Templates[i].Metadata.Name == "page1_stats.html" { + if templateSet.Templates[i].Metadata.Name == "page_1.html" { page1Template = &templateSet.Templates[i] break } } if page1Template == nil { - t.Fatal("page1_stats.html template not found") + t.Fatal("page_1.html template not found") } var col1Capacity int for i := range page1Template.Metadata.Blocks { diff --git a/backend/pdfrender/handlers.go b/backend/pdfrender/handlers.go index ae976dd..0954f85 100644 --- a/backend/pdfrender/handlers.go +++ b/backend/pdfrender/handlers.go @@ -91,7 +91,7 @@ func ExportCharacterToPDF(c *gin.Context) { var allPDFs [][]byte // Page 1: Stats - page1PDFs, err := RenderPageWithContinuations(viewModel, "page1_stats.html", 1, currentDate, loader, renderer) + page1PDFs, err := RenderPageWithContinuations(viewModel, "page_1.html", 1, currentDate, loader, renderer) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to render page 1: " + err.Error()}) return @@ -99,7 +99,7 @@ func ExportCharacterToPDF(c *gin.Context) { allPDFs = append(allPDFs, page1PDFs...) // Page 2: Play - page2PDFs, err := RenderPageWithContinuations(viewModel, "page2_play.html", 2, currentDate, loader, renderer) + page2PDFs, err := RenderPageWithContinuations(viewModel, "page_2.html", 2, currentDate, loader, renderer) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to render page 2: " + err.Error()}) return @@ -107,7 +107,7 @@ func ExportCharacterToPDF(c *gin.Context) { allPDFs = append(allPDFs, page2PDFs...) // Page 3: Spells - page3PDFs, err := RenderPageWithContinuations(viewModel, "page3_spell.html", 3, currentDate, loader, renderer) + page3PDFs, err := RenderPageWithContinuations(viewModel, "page_3.html", 3, currentDate, loader, renderer) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to render page 3: " + err.Error()}) return @@ -115,7 +115,7 @@ func ExportCharacterToPDF(c *gin.Context) { allPDFs = append(allPDFs, page3PDFs...) // Page 4: Equipment - page4PDFs, err := RenderPageWithContinuations(viewModel, "page4_equip.html", 4, currentDate, loader, renderer) + page4PDFs, err := RenderPageWithContinuations(viewModel, "page_4.html", 4, currentDate, loader, renderer) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to render page 4: " + err.Error()}) return diff --git a/backend/pdfrender/integration_test.go b/backend/pdfrender/integration_test.go index 1ac0dde..14f62a3 100644 --- a/backend/pdfrender/integration_test.go +++ b/backend/pdfrender/integration_test.go @@ -101,12 +101,12 @@ func TestIntegration_FullPDFGeneration(t *testing.T) { } // Step 3: Prepare paginated data and render template to HTML - pageData, err := PreparePaginatedPageData(viewModel, "page1_stats.html", 1, "18.12.2025") + pageData, err := PreparePaginatedPageData(viewModel, "page_1.html", 1, "18.12.2025") if err != nil { t.Fatalf("Failed to prepare paginated data: %v", err) } - html, err := loader.RenderTemplate("page1_stats.html", pageData) + html, err := loader.RenderTemplate("page_1.html", pageData) if err != nil { t.Fatalf("Failed to render template: %v", err) } @@ -160,11 +160,11 @@ func TestIntegration_TemplateMetadata(t *testing.T) { template string expectedBlock string }{ - {"page1_stats.html", "skills_column1"}, - {"page2_play.html", "skills_learned"}, - {"page3_spell.html", "spells_left"}, - {"page3_spell.html", "spells_right"}, - {"page4_equip.html", "equipment_worn"}, + {"page_1.html", "skills_column1"}, + {"page_2.html", "skills_learned"}, + {"page_3.html", "spells_left"}, + {"page_3.html", "spells_right"}, + {"page_4.html", "equipment_worn"}, } for _, tc := range testCases { @@ -217,7 +217,7 @@ func TestIntegration_PaginationWithPDF(t *testing.T) { templateSet := DefaultA4QuerTemplateSet() paginator := NewPaginator(templateSet) - pages, err := paginator.PaginateSkills(skills, "page1_stats.html", "") + pages, err := paginator.PaginateSkills(skills, "page_1.html", "") if err != nil { t.Fatalf("Failed to paginate skills: %v", err) } @@ -225,7 +225,7 @@ func TestIntegration_PaginationWithPDF(t *testing.T) { // Calculate expected pages based on actual capacity var skillsCapacity int for _, tmpl := range templateSet.Templates { - if tmpl.Metadata.Name == "page1_stats.html" { + if tmpl.Metadata.Name == "page_1.html" { for _, block := range tmpl.Metadata.Blocks { if block.ListType == "skills" { skillsCapacity += block.MaxItems @@ -275,7 +275,7 @@ func TestIntegration_PaginationWithPDF(t *testing.T) { pageData.SkillsColumn2 = pages[0].Data["skills_column2"].([]SkillViewModel) pageData.Skills = append(pageData.SkillsColumn1, pageData.SkillsColumn2...) // Keep for logging - html, err := loader.RenderTemplate("page1_stats.html", pageData) + html, err := loader.RenderTemplate("page_1.html", pageData) if err != nil { t.Fatalf("Failed to render template: %v", err) } @@ -325,7 +325,7 @@ func TestIntegration_MultiPageSpellList(t *testing.T) { templateSet := DefaultA4QuerTemplateSet() var page3Template *TemplateWithMeta for i := range templateSet.Templates { - if templateSet.Templates[i].Metadata.Name == "page3_spell.html" { + if templateSet.Templates[i].Metadata.Name == "page_3.html" { page3Template = &templateSet.Templates[i] break } @@ -351,7 +351,7 @@ func TestIntegration_MultiPageSpellList(t *testing.T) { paginator := NewPaginator(templateSet) // Paginate spells - pages, err := paginator.PaginateSpells(spells, "page3_spell.html") + pages, err := paginator.PaginateSpells(spells, "page_3.html") if err != nil { t.Fatalf("Failed to paginate spells: %v", err) } @@ -446,7 +446,7 @@ func TestIntegration_CompleteWorkflow(t *testing.T) { paginator := NewPaginator(templateSet) // Step 4: Paginate skills - skillPages, err := paginator.PaginateSkills(viewModel.Skills, "page1_stats.html", "") + skillPages, err := paginator.PaginateSkills(viewModel.Skills, "page_1.html", "") if err != nil { t.Fatalf("Failed to paginate skills: %v", err) } @@ -555,7 +555,7 @@ func TestVisualInspection_AllPages(t *testing.T) { // Page 1: Stats page with skills (may have continuations) t.Log("Generating Page 1: Stats...") - page1PDFs, err := RenderPageWithContinuations(viewModel, "page1_stats.html", 1, "18.12.2025", loader, renderer) + page1PDFs, err := RenderPageWithContinuations(viewModel, "page_1.html", 1, "18.12.2025", loader, renderer) if err != nil { t.Fatalf("Failed to generate page1: %v", err) } @@ -580,7 +580,7 @@ func TestVisualInspection_AllPages(t *testing.T) { // Page 2: Play/Adventure page with weapons (may have continuations) t.Log("Generating Page 2: Play...") - page2PDFs, err := RenderPageWithContinuations(viewModel, "page2_play.html", 2, "18.12.2025", loader, renderer) + page2PDFs, err := RenderPageWithContinuations(viewModel, "page_2.html", 2, "18.12.2025", loader, renderer) if err != nil { t.Fatalf("Failed to generate page2: %v", err) } @@ -605,7 +605,7 @@ func TestVisualInspection_AllPages(t *testing.T) { // Page 3: Spells page (may have continuations) t.Log("Generating Page 3: Spells...") - page3PDFs, err := RenderPageWithContinuations(viewModel, "page3_spell.html", 3, "18.12.2025", loader, renderer) + page3PDFs, err := RenderPageWithContinuations(viewModel, "page_3.html", 3, "18.12.2025", loader, renderer) if err != nil { t.Fatalf("Failed to generate page3: %v", err) } @@ -630,7 +630,7 @@ func TestVisualInspection_AllPages(t *testing.T) { // Page 4: Equipment page (may have continuations) t.Log("Generating Page 4: Equipment...") - page4PDFs, err := RenderPageWithContinuations(viewModel, "page4_equip.html", 4, "18.12.2025", loader, renderer) + page4PDFs, err := RenderPageWithContinuations(viewModel, "page_4.html", 4, "18.12.2025", loader, renderer) if err != nil { t.Fatalf("Failed to generate page4: %v", err) } diff --git a/backend/pdfrender/pagination.go b/backend/pdfrender/pagination.go index 8c16da0..4464b1d 100644 --- a/backend/pdfrender/pagination.go +++ b/backend/pdfrender/pagination.go @@ -6,55 +6,51 @@ import ( ) // GenerateContinuationTemplateName creates a continuation template name -// Example: "page1_stats.html" + pageNum 2 -> "page1.2_stats.html" -// Note: All continuation pages (2, 3, 4, ...) use the same template name: page1.2_stats.html +// Example: "page_1.html" + pageNum 2 -> "page_1.2.html" +// Note: All continuation pages (2, 3, 4, ...) use the same template name: page_1.2.html func GenerateContinuationTemplateName(originalTemplate string, pageNum int) string { if pageNum == 1 { return originalTemplate } - // All continuation pages use .2 template (page1.2, page2.2, etc.) - // NOT page1.3, page1.4, etc. + // All continuation pages use .2 template (page_1.2, page_2.2, etc.) + // NOT page_1.3, page_1.4, etc. - // Split template name at first underscore to insert page continuation number - // Example: "page1_stats.html" -> "page1" + "_stats.html" - parts := strings.SplitN(originalTemplate, "_", 2) - if len(parts) != 2 { - // Fallback: just append .2 before extension - ext := ".html" - base := strings.TrimSuffix(originalTemplate, ext) - return fmt.Sprintf("%s.2%s", base, ext) + // New format: "page_1.html" -> "page_1.2.html" + // Pattern: page_N.html where N is a number + ext := ".html" + base := strings.TrimSuffix(originalTemplate, ext) + + // Check if it's already a continuation (has .2 in it) + if strings.Contains(base, ".2") { + return originalTemplate } - - // Extract page number and base name - // "page1" -> "page" + "1" - baseName := parts[0] - suffix := parts[1] - - // Always use .2 for continuation pages: "page1.2_stats.html" - return fmt.Sprintf("%s.2_%s", baseName, suffix) + + // Append .2 before .html + return fmt.Sprintf("%s.2%s", base, ext) } // ExtractBaseTemplateName extracts the base template name from a continuation template -// Example: "page1.2_stats.html" -> "page1_stats.html" +// Example: "page_1.2.html" -> "page_1.html" func ExtractBaseTemplateName(templateName string) string { - // Check if it's a continuation template (contains .N_ pattern) - parts := strings.SplitN(templateName, "_", 2) - if len(parts) != 2 { + // New format: "page_1.2.html" or "page_1.10.html" -> "page_1.html" + // Pattern: ends with .N.html where N is any number + ext := ".html" + if !strings.HasSuffix(templateName, ext) { return templateName } - baseName := parts[0] - suffix := parts[1] - - // Check if baseName contains a dot followed by a number (e.g., "page1.2") - dotIdx := strings.LastIndex(baseName, ".") - if dotIdx == -1 { - return templateName // Not a continuation template + // Remove .html + base := strings.TrimSuffix(templateName, ext) + + // Find the last dot + lastDotIdx := strings.LastIndex(base, ".") + if lastDotIdx == -1 { + return templateName // No dot found, not a continuation } - // Verify the part after the dot is a number - numPart := baseName[dotIdx+1:] + // Check if everything after the last dot is a number + numPart := base[lastDotIdx+1:] if len(numPart) == 0 { return templateName } @@ -66,8 +62,7 @@ func ExtractBaseTemplateName(templateName string) string { } // It's a continuation template, return the base name - basePrefix := baseName[:dotIdx] - return fmt.Sprintf("%s_%s", basePrefix, suffix) + return base[:lastDotIdx] + ext } // SliceList slices a list based on start index and max items @@ -106,6 +101,189 @@ func NewPaginator(templateSet TemplateSet) *Paginator { } } +// PaginateMultiList is a unified pagination function that handles multiple list types +// It replaces PaginateSkills, PaginateSpells, and PaginatePage2PlayLists +// dataMap keys: "skills", "weapons", "spells", "equipment", "magicItems" +func (p *Paginator) PaginateMultiList(dataMap map[string]interface{}, templateName string) ([]PageDistribution, error) { + template := p.findTemplate(templateName) + if template == nil { + return nil, fmt.Errorf("template not found: %s", templateName) + } + + // Build filtered lists for each unique list type + filter combination + type listTracker struct { + items interface{} + currentIdx int + totalCount int + } + + // Track by "listType:filter" to avoid duplicates + listTrackers := make(map[string]*listTracker) + + // First pass: create filtered lists for each unique listType+filter combination + for _, block := range template.Blocks { + // Get the source data based on block's ListType + var sourceData interface{} + switch block.ListType { + case "skills": + sourceData = dataMap["skills"] + case "weapons": + sourceData = dataMap["weapons"] + case "spells": + sourceData = dataMap["spells"] + case "equipment": + sourceData = dataMap["equipment"] + case "magicItems": + sourceData = dataMap["magicItems"] + default: + continue + } + + if sourceData == nil { + continue + } + + // Create unique key for this list type + filter combination + trackerKey := block.ListType + if block.Filter != "" { + trackerKey += ":" + block.Filter + } + + // Only create tracker once per unique combination + if _, exists := listTrackers[trackerKey]; !exists { + // Apply filter if specified + filteredItems := p.applyFilter(sourceData, block.Filter) + itemCount := p.getItemCount(filteredItems) + + if itemCount > 0 { + listTrackers[trackerKey] = &listTracker{ + items: filteredItems, + currentIdx: 0, + totalCount: itemCount, + } + } + } + } + + // If all lists are empty, return empty result + if len(listTrackers) == 0 { + return []PageDistribution{}, nil + } + + // Generate pages until all items are distributed + distributions := []PageDistribution{} + pageNum := 1 + + for { + // Check if there are any remaining items + hasRemainingItems := false + for _, tracker := range listTrackers { + if tracker.currentIdx < tracker.totalCount { + hasRemainingItems = true + break + } + } + + if !hasRemainingItems { + break + } + + // Create page data + pageData := make(map[string]interface{}) + + // Distribute items to each block for this page + for _, block := range template.Blocks { + // Get tracker for this block's list type + filter + trackerKey := block.ListType + if block.Filter != "" { + trackerKey += ":" + block.Filter + } + + tracker, exists := listTrackers[trackerKey] + if !exists { + // Block has no data, add empty slice + pageData[block.Name] = p.createEmptySlice(block.ListType) + continue + } + + // Calculate how many items to take for this block + itemsToTake := block.MaxItems + remaining := tracker.totalCount - tracker.currentIdx + if itemsToTake > remaining { + itemsToTake = remaining + } + + // Extract slice for this block + blockItems := p.extractSlice(tracker.items, tracker.currentIdx, itemsToTake) + pageData[block.Name] = blockItems + tracker.currentIdx += itemsToTake + } + + // Determine template name - use continuation naming for pages 2+ + pageTemplateName := GenerateContinuationTemplateName(templateName, pageNum) + + distributions = append(distributions, PageDistribution{ + TemplateName: pageTemplateName, + PageNumber: pageNum, + Data: pageData, + }) + + pageNum++ + } + + return distributions, nil +} + +// applyFilter filters a list based on filter criteria +func (p *Paginator) applyFilter(items interface{}, filter string) interface{} { + if filter == "" { + return items + } + + switch v := items.(type) { + case []SkillViewModel: + filtered := []SkillViewModel{} + for _, skill := range v { + include := false + switch filter { + case "learned": + include = skill.IsLearned && skill.Category != "Sprache" + case "unlearned": + include = !skill.IsLearned && skill.Category != "Sprache" + case "language", "languages": + include = skill.Category == "Sprache" + default: + include = true + } + if include { + filtered = append(filtered, skill) + } + } + return filtered + default: + // No filtering for other types + return items + } +} + +// getItemCount returns the count of items in a list +func (p *Paginator) getItemCount(items interface{}) int { + switch v := items.(type) { + case []SkillViewModel: + return len(v) + case []WeaponViewModel: + return len(v) + case []SpellViewModel: + return len(v) + case []EquipmentViewModel: + return len(v) + case []MagicItemViewModel: + return len(v) + default: + return 0 + } +} + // PaginateSkills splits skills across multiple pages according to template capacity func (p *Paginator) PaginateSkills(skills []SkillViewModel, templateName string, filter string) ([]PageDistribution, error) { template := p.findTemplate(templateName) @@ -289,6 +467,12 @@ func (p *Paginator) extractSlice(items interface{}, start, count int) interface{ end = len(v) } return v[start:end] + case []MagicItemViewModel: + end := start + count + if end > len(v) { + end = len(v) + } + return v[start:end] } return nil } @@ -304,6 +488,8 @@ func (p *Paginator) createEmptySlice(listType string) interface{} { return []SpellViewModel{} case "equipment": return []EquipmentViewModel{} + case "magicItems": + return []MagicItemViewModel{} default: return []interface{}{} } diff --git a/backend/pdfrender/pagination_helper.go b/backend/pdfrender/pagination_helper.go index c1c81a8..3262b01 100644 --- a/backend/pdfrender/pagination_helper.go +++ b/backend/pdfrender/pagination_helper.go @@ -32,7 +32,7 @@ func PreparePaginatedPageData(viewModel *CharacterSheetViewModel, templateName s } // For page1_stats.html - paginate skills across two columns - if templateName == "page1_stats.html" { + if templateName == "page_1.html" { // Get the template metadata var template *TemplateMetadata for _, tmpl := range templateSet.Templates { @@ -70,7 +70,7 @@ func PreparePaginatedPageData(viewModel *CharacterSheetViewModel, templateName s } else { pageData.Skills = viewModel.Skills } - } else if templateName == "page2_play.html" { + } else if templateName == "page_2.html" { // Get capacities from template weaponsCapacity := GetBlockCapacity(&templateSet, templateName, "weapons_main") learnedCapacity := GetBlockCapacity(&templateSet, templateName, "skills_learned") @@ -117,7 +117,7 @@ func PreparePaginatedPageData(viewModel *CharacterSheetViewModel, templateName s pageData.SkillsLanguage = languageSkills } pageData.Skills = viewModel.Skills // Keep for backward compatibility - } else if templateName == "page3_spell.html" { + } else if templateName == "page_3.html" { // Get capacities from template spellsLeftCapacity := GetBlockCapacity(&templateSet, templateName, "spells_left") spellsRightCapacity := GetBlockCapacity(&templateSet, templateName, "spells_right") @@ -148,7 +148,7 @@ func PreparePaginatedPageData(viewModel *CharacterSheetViewModel, templateName s } else { pageData.MagicItems = magicItems } - } else if templateName == "page4_equip.html" { + } else if templateName == "page_4.html" { // Page 4 needs ALL equipment to properly render containers // The template has complex logic showing containers on left, worn items and container sections on right // Don't truncate based on capacity - let the template handle all items diff --git a/backend/pdfrender/pagination_helper_test.go b/backend/pdfrender/pagination_helper_test.go index 24f6782..1d70c16 100644 --- a/backend/pdfrender/pagination_helper_test.go +++ b/backend/pdfrender/pagination_helper_test.go @@ -18,7 +18,7 @@ func TestPreparePaginatedPageData_Page1Stats(t *testing.T) { } } - pageData, err := PreparePaginatedPageData(viewModel, "page1_stats.html", 1, "2024-01-01") + pageData, err := PreparePaginatedPageData(viewModel, "page_1.html", 1, "2024-01-01") if err != nil { t.Fatalf("PreparePaginatedPageData failed: %v", err) } @@ -36,7 +36,7 @@ func TestPreparePaginatedPageData_Page1Stats(t *testing.T) { templateSet := DefaultA4QuerTemplateSet() var page1Template *TemplateWithMeta for i := range templateSet.Templates { - if templateSet.Templates[i].Metadata.Name == "page1_stats.html" { + if templateSet.Templates[i].Metadata.Name == "page_1.html" { page1Template = &templateSet.Templates[i] break } @@ -75,7 +75,7 @@ func TestSplitSkillsForColumns(t *testing.T) { templateSet := DefaultA4QuerTemplateSet() var page1Template *TemplateWithMeta for i := range templateSet.Templates { - if templateSet.Templates[i].Metadata.Name == "page1_stats.html" { + if templateSet.Templates[i].Metadata.Name == "page_1.html" { page1Template = &templateSet.Templates[i] break } @@ -174,7 +174,7 @@ func TestPreparePaginatedPageData_Page2Play(t *testing.T) { } } - pageData, err := PreparePaginatedPageData(viewModel, "page2_play.html", 2, "2024-01-01") + pageData, err := PreparePaginatedPageData(viewModel, "page_2.html", 2, "2024-01-01") if err != nil { t.Fatalf("PreparePaginatedPageData failed: %v", err) } @@ -190,9 +190,9 @@ func TestPreparePaginatedPageData_Page2Play(t *testing.T) { func TestPreparePaginatedPageData_Page3Spell(t *testing.T) { // Get capacities from template templateSet := DefaultA4QuerTemplateSet() - leftCap := GetBlockCapacity(&templateSet, "page3_spell.html", "spells_left") - rightCap := GetBlockCapacity(&templateSet, "page3_spell.html", "spells_right") - magicItemsCap := GetBlockCapacity(&templateSet, "page3_spell.html", "magic_items") + leftCap := GetBlockCapacity(&templateSet, "page_3.html", "spells_left") + rightCap := GetBlockCapacity(&templateSet, "page_3.html", "spells_right") + magicItemsCap := GetBlockCapacity(&templateSet, "page_3.html", "magic_items") // Create test data exceeding capacities viewModel := &CharacterSheetViewModel{ @@ -210,7 +210,7 @@ func TestPreparePaginatedPageData_Page3Spell(t *testing.T) { } } - pageData, err := PreparePaginatedPageData(viewModel, "page3_spell.html", 3, "2024-01-01") + pageData, err := PreparePaginatedPageData(viewModel, "page_3.html", 3, "2024-01-01") if err != nil { t.Fatalf("PreparePaginatedPageData failed: %v", err) } @@ -247,7 +247,7 @@ func TestPreparePaginatedPageData_Page4Equipment(t *testing.T) { } } - pageData, err := PreparePaginatedPageData(viewModel, "page4_equip.html", 4, "2024-01-01") + pageData, err := PreparePaginatedPageData(viewModel, "page_4.html", 4, "2024-01-01") if err != nil { t.Fatalf("PreparePaginatedPageData failed: %v", err) } diff --git a/backend/pdfrender/pagination_test.go b/backend/pdfrender/pagination_test.go index ab7db68..0ec3cd5 100644 --- a/backend/pdfrender/pagination_test.go +++ b/backend/pdfrender/pagination_test.go @@ -5,6 +5,314 @@ import ( "testing" ) +// TestPaginateMultiList_SingleListType tests pagination with a single list type (skills only) +func TestPaginateMultiList_SingleListType(t *testing.T) { + // Arrange + templateSet := DefaultA4QuerTemplateSet() + paginator := NewPaginator(templateSet) + + // Create skills that will fit on one page (less than total capacity) + skills := make([]SkillViewModel, 20) + for i := 0; i < 20; i++ { + skills[i] = SkillViewModel{Name: fmt.Sprintf("Skill%d", i+1)} + } + + dataMap := map[string]interface{}{ + "skills": skills, + } + + // Act + distributions, err := paginator.PaginateMultiList(dataMap, "page_1.html") + + // Assert + if err != nil { + t.Fatalf("Expected no error, got %v", err) + } + + if len(distributions) != 1 { + t.Fatalf("Expected 1 page, got %d", len(distributions)) + } + + // Verify data is distributed correctly + page := distributions[0] + if page.TemplateName != "page_1.html" { + t.Errorf("Expected template 'page_1.html', got '%s'", page.TemplateName) + } + + // Check that skills are distributed to appropriate blocks + // Note: Skills are distributed to multiple columns, so we count each block + totalSkills := 0 + skillBlocks := []string{"skills_column1", "skills_column2"} + for _, blockName := range skillBlocks { + if data, exists := page.Data[blockName]; exists { + if skillsList, ok := data.([]SkillViewModel); ok { + totalSkills += len(skillsList) + } + } + } + + if totalSkills != 20 { + t.Errorf("Expected 20 total skills distributed across columns, got %d", totalSkills) + } +} + +// TestPaginateMultiList_MultipleListTypes tests pagination with multiple list types +func TestPaginateMultiList_MultipleListTypes(t *testing.T) { + // Arrange + templateSet := DefaultA4QuerTemplateSet() + paginator := NewPaginator(templateSet) + + // Create skills and weapons for page_2.html + skills := make([]SkillViewModel, 30) + for i := 0; i < 30; i++ { + skills[i] = SkillViewModel{ + Name: fmt.Sprintf("Skill%d", i+1), + IsLearned: i < 15, // First 15 are learned + Category: "Kampf", + } + } + + weapons := make([]WeaponViewModel, 10) + for i := 0; i < 10; i++ { + weapons[i] = WeaponViewModel{ + Name: fmt.Sprintf("Weapon%d", i+1), + Value: 10 + i, + } + } + + dataMap := map[string]interface{}{ + "skills": skills, + "weapons": weapons, + } + + // Act + distributions, err := paginator.PaginateMultiList(dataMap, "page_2.html") + + // Assert + if err != nil { + t.Fatalf("Expected no error, got %v", err) + } + + if len(distributions) == 0 { + t.Fatal("Expected at least 1 page") + } + + // Verify all data is distributed across pages + totalSkills := 0 + totalWeapons := 0 + + for _, dist := range distributions { + for _, data := range dist.Data { + switch v := data.(type) { + case []SkillViewModel: + totalSkills += len(v) + case []WeaponViewModel: + totalWeapons += len(v) + } + } + } + + if totalSkills != 30 { + t.Errorf("Expected 30 total skills, got %d", totalSkills) + } + if totalWeapons != 10 { + t.Errorf("Expected 10 total weapons, got %d", totalWeapons) + } +} + +// TestPaginateMultiList_WithOverflow tests continuation pages are created +func TestPaginateMultiList_WithOverflow(t *testing.T) { + // Arrange + templateSet := DefaultA4QuerTemplateSet() + paginator := NewPaginator(templateSet) + + // Create many skills to force continuation pages + skills := make([]SkillViewModel, 100) + for i := 0; i < 100; i++ { + skills[i] = SkillViewModel{Name: fmt.Sprintf("Skill%d", i+1)} + } + + dataMap := map[string]interface{}{ + "skills": skills, + } + + // Act + distributions, err := paginator.PaginateMultiList(dataMap, "page_1.html") + + // Assert + if err != nil { + t.Fatalf("Expected no error, got %v", err) + } + + if len(distributions) <= 1 { + t.Fatalf("Expected multiple pages for 100 skills, got %d", len(distributions)) + } + + // Verify continuation template naming + if distributions[0].TemplateName != "page_1.html" { + t.Errorf("First page should use base template, got '%s'", distributions[0].TemplateName) + } + + for i := 1; i < len(distributions); i++ { + expectedName := "page_1.2.html" + if distributions[i].TemplateName != expectedName { + t.Errorf("Page %d should use continuation template '%s', got '%s'", + i+1, expectedName, distributions[i].TemplateName) + } + } + + // Verify all skills are distributed + totalSkills := 0 + for _, dist := range distributions { + for blockName, data := range dist.Data { + // Only count skill blocks to avoid counting the same skills multiple times + if skillsList, ok := data.([]SkillViewModel); ok && + (blockName == "skills_column1" || blockName == "skills_column2" || + blockName == "skills_column3" || blockName == "skills_column4") { + totalSkills += len(skillsList) + } + } + } + + if totalSkills != 100 { + t.Errorf("Expected 100 total skills distributed, got %d", totalSkills) + } +} + +// TestPaginateMultiList_EmptyLists tests handling of empty data +func TestPaginateMultiList_EmptyLists(t *testing.T) { + // Arrange + templateSet := DefaultA4QuerTemplateSet() + paginator := NewPaginator(templateSet) + + dataMap := map[string]interface{}{ + "skills": []SkillViewModel{}, + } + + // Act + distributions, err := paginator.PaginateMultiList(dataMap, "page_1.html") + + // Assert + if err != nil { + t.Fatalf("Expected no error, got %v", err) + } + + if len(distributions) != 0 { + t.Errorf("Expected 0 pages for empty data, got %d", len(distributions)) + } +} + +// TestPaginateMultiList_WithFilters tests filtering by learned/unlearned/language +func TestPaginateMultiList_WithFilters(t *testing.T) { + // Arrange + templateSet := DefaultA4QuerTemplateSet() + paginator := NewPaginator(templateSet) + + // Create mixed skills + skills := []SkillViewModel{ + {Name: "Learned1", IsLearned: true, Category: "Kampf"}, + {Name: "Learned2", IsLearned: true, Category: "Körper"}, + {Name: "Unlearned1", IsLearned: false, Category: "Kampf"}, + {Name: "Unlearned2", IsLearned: false, Category: "Social"}, + {Name: "Language1", Category: "Sprache"}, + {Name: "Language2", Category: "Sprache"}, + } + + dataMap := map[string]interface{}{ + "skills": skills, + } + + // Act + distributions, err := paginator.PaginateMultiList(dataMap, "page_2.html") + + // Assert + if err != nil { + t.Fatalf("Expected no error, got %v", err) + } + + if len(distributions) == 0 { + t.Fatal("Expected at least 1 page") + } + + // Verify filtering worked - check that learned, unlearned, and language skills are in separate blocks + page := distributions[0] + + if learnedData, ok := page.Data["skills_learned"]; ok { + learned := learnedData.([]SkillViewModel) + for _, skill := range learned { + if !skill.IsLearned { + t.Errorf("Found unlearned skill '%s' in learned block", skill.Name) + } + } + } + + if languageData, ok := page.Data["skills_languages"]; ok { + languages := languageData.([]SkillViewModel) + for _, skill := range languages { + if skill.Category != "Sprache" { + t.Errorf("Found non-language skill '%s' in language block", skill.Name) + } + } + } +} + +// TestPaginateMultiList_UnknownTemplate tests error handling for unknown templates +func TestPaginateMultiList_UnknownTemplate(t *testing.T) { + // Arrange + templateSet := DefaultA4QuerTemplateSet() + paginator := NewPaginator(templateSet) + + dataMap := map[string]interface{}{ + "skills": []SkillViewModel{{Name: "Test"}}, + } + + // Act + distributions, err := paginator.PaginateMultiList(dataMap, "nonexistent_template.html") + + // Assert + if err == nil { + t.Fatal("Expected error for unknown template, got nil") + } + + if distributions != nil { + t.Error("Expected nil distributions for error case") + } +} + +// TestPaginateMultiList_PageNumbering tests that page numbers are sequential +func TestPaginateMultiList_PageNumbering(t *testing.T) { + // Arrange + templateSet := DefaultA4QuerTemplateSet() + paginator := NewPaginator(templateSet) + + // Create enough skills for 3 pages + skills := make([]SkillViewModel, 120) + for i := 0; i < 120; i++ { + skills[i] = SkillViewModel{Name: fmt.Sprintf("Skill%d", i+1)} + } + + dataMap := map[string]interface{}{ + "skills": skills, + } + + // Act + distributions, err := paginator.PaginateMultiList(dataMap, "page_1.html") + + // Assert + if err != nil { + t.Fatalf("Expected no error, got %v", err) + } + + // Verify page numbers are sequential + for i, dist := range distributions { + expectedPageNum := i + 1 + if dist.PageNumber != expectedPageNum { + t.Errorf("Page %d has incorrect PageNumber: expected %d, got %d", + i+1, expectedPageNum, dist.PageNumber) + } + } +} + func TestSliceList_Basic(t *testing.T) { // Arrange items := []int{1, 2, 3, 4, 5, 6, 7, 8, 9, 10} @@ -64,7 +372,7 @@ func TestPaginateSkills_SinglePage(t *testing.T) { // Get column capacities from template var col1Capacity int for _, tmpl := range templateSet.Templates { - if tmpl.Metadata.Name == "page1_stats.html" { + if tmpl.Metadata.Name == "page_1.html" { for _, block := range tmpl.Metadata.Blocks { if block.Name == "skills_column1" { col1Capacity = block.MaxItems @@ -83,7 +391,7 @@ func TestPaginateSkills_SinglePage(t *testing.T) { } // Act - pages, err := paginator.PaginateSkills(skills, "page1_stats.html", "") + pages, err := paginator.PaginateSkills(skills, "page_1.html", "") // Assert if err != nil { @@ -127,7 +435,7 @@ func TestPaginateSkills_MultiColumn(t *testing.T) { // Get expected capacity from template var page1Template *TemplateWithMeta for i := range templateSet.Templates { - if templateSet.Templates[i].Metadata.Name == "page1_stats.html" { + if templateSet.Templates[i].Metadata.Name == "page_1.html" { page1Template = &templateSet.Templates[i] break } @@ -149,7 +457,7 @@ func TestPaginateSkills_MultiColumn(t *testing.T) { } // Act - pages, err := paginator.PaginateSkills(skills, "page1_stats.html", "") + pages, err := paginator.PaginateSkills(skills, "page_1.html", "") // Assert if err != nil { @@ -186,7 +494,7 @@ func TestPaginateSkills_MultiPage(t *testing.T) { // Get column capacities from template var page1Template *TemplateWithMeta for i := range templateSet.Templates { - if templateSet.Templates[i].Metadata.Name == "page1_stats.html" { + if templateSet.Templates[i].Metadata.Name == "page_1.html" { page1Template = &templateSet.Templates[i] break } @@ -203,7 +511,7 @@ func TestPaginateSkills_MultiPage(t *testing.T) { } // Act - pages, err := paginator.PaginateSkills(skills, "page1_stats.html", "") + pages, err := paginator.PaginateSkills(skills, "page_1.html", "") // Assert if err != nil { @@ -247,7 +555,7 @@ func TestPaginateSpells_TwoColumns(t *testing.T) { // Get spell column capacities from template var page3Template *TemplateWithMeta for i := range templateSet.Templates { - if templateSet.Templates[i].Metadata.Name == "page3_spell.html" { + if templateSet.Templates[i].Metadata.Name == "page_3.html" { page3Template = &templateSet.Templates[i] break } @@ -271,7 +579,7 @@ func TestPaginateSpells_TwoColumns(t *testing.T) { } // Act - pages, err := paginator.PaginateSpells(spells, "page3_spell.html") + pages, err := paginator.PaginateSpells(spells, "page_3.html") // Assert if err != nil { @@ -311,7 +619,7 @@ func TestPaginateSpells_MultiPage(t *testing.T) { // Get spell column capacities from template var page3Template *TemplateWithMeta for i := range templateSet.Templates { - if templateSet.Templates[i].Metadata.Name == "page3_spell.html" { + if templateSet.Templates[i].Metadata.Name == "page_3.html" { page3Template = &templateSet.Templates[i] break } @@ -332,7 +640,7 @@ func TestPaginateSpells_MultiPage(t *testing.T) { } // Act - pages, err := paginator.PaginateSpells(spells, "page3_spell.html") + pages, err := paginator.PaginateSpells(spells, "page_3.html") // Assert if err != nil { @@ -366,7 +674,7 @@ func TestPaginateWeapons_SinglePage(t *testing.T) { // Get weapon capacity from template var weaponCapacity int for _, tmpl := range templateSet.Templates { - if tmpl.Metadata.Name == "page2_play.html" { + if tmpl.Metadata.Name == "page_2.html" { for _, block := range tmpl.Metadata.Blocks { if block.ListType == "weapons" { weaponCapacity = block.MaxItems @@ -388,7 +696,7 @@ func TestPaginateWeapons_SinglePage(t *testing.T) { } // Act - pages, err := paginator.PaginateWeapons(weapons, "page2_play.html") + pages, err := paginator.PaginateWeapons(weapons, "page_2.html") // Assert if err != nil { @@ -415,13 +723,13 @@ func TestPaginateWeapons_MultiPage(t *testing.T) { // Get weapon capacity from template var page2Template *TemplateWithMeta for i := range templateSet.Templates { - if templateSet.Templates[i].Metadata.Name == "page2_play.html" { + if templateSet.Templates[i].Metadata.Name == "page_2.html" { page2Template = &templateSet.Templates[i] break } } if page2Template == nil { - t.Fatal("page2_play.html template not found") + t.Fatal("page_2.html template not found") } var weaponsBlock *BlockMetadata for i := range page2Template.Metadata.Blocks { @@ -442,7 +750,7 @@ func TestPaginateWeapons_MultiPage(t *testing.T) { } // Act - pages, err := paginator.PaginateWeapons(weapons, "page2_play.html") + pages, err := paginator.PaginateWeapons(weapons, "page_2.html") // Assert if err != nil { @@ -479,11 +787,11 @@ func TestCalculatePagesNeeded(t *testing.T) { var page1Template, page2Template, page3Template *TemplateWithMeta for i := range templateSet.Templates { switch templateSet.Templates[i].Metadata.Name { - case "page1_stats.html": + case "page_1.html": page1Template = &templateSet.Templates[i] - case "page2_play.html": + case "page_2.html": page2Template = &templateSet.Templates[i] - case "page3_spell.html": + case "page_3.html": page3Template = &templateSet.Templates[i] } } @@ -526,16 +834,16 @@ func TestCalculatePagesNeeded(t *testing.T) { itemCount int expectedPages int }{ - {"10 skills on page1", "page1_stats.html", "skills", 10, 1}, - {fmt.Sprintf("%d skills on page1", skillCapacity), "page1_stats.html", "skills", skillCapacity, 1}, - {fmt.Sprintf("%d skills on page1", skillCapacity+1), "page1_stats.html", "skills", skillCapacity + 1, 2}, - {"100 skills on page1", "page1_stats.html", "skills", 100, (100 + skillCapacity - 1) / skillCapacity}, // Dynamic calculation - {"10 weapons on page2", "page2_play.html", "weapons", 10, (10 + weaponCapacity - 1) / weaponCapacity}, // Dynamic calculation - {fmt.Sprintf("%d weapons on page2", weaponCapacity), "page2_play.html", "weapons", weaponCapacity, 1}, - {fmt.Sprintf("%d weapons on page2", weaponCapacity+1), "page2_play.html", "weapons", weaponCapacity + 1, 2}, - {"10 spells on page3", "page3_spell.html", "spells", 10, (10 + spellCapacity - 1) / spellCapacity}, // Dynamic calculation - {fmt.Sprintf("%d spells on page3", spellCapacity), "page3_spell.html", "spells", spellCapacity, 1}, - {fmt.Sprintf("%d spells on page3", spellCapacity+1), "page3_spell.html", "spells", spellCapacity + 1, 2}, + {"10 skills on page1", "page_1.html", "skills", 10, 1}, + {fmt.Sprintf("%d skills on page1", skillCapacity), "page_1.html", "skills", skillCapacity, 1}, + {fmt.Sprintf("%d skills on page1", skillCapacity+1), "page_1.html", "skills", skillCapacity + 1, 2}, + {"100 skills on page1", "page_1.html", "skills", 100, (100 + skillCapacity - 1) / skillCapacity}, // Dynamic calculation + {"10 weapons on page2", "page_2.html", "weapons", 10, (10 + weaponCapacity - 1) / weaponCapacity}, // Dynamic calculation + {fmt.Sprintf("%d weapons on page2", weaponCapacity), "page_2.html", "weapons", weaponCapacity, 1}, + {fmt.Sprintf("%d weapons on page2", weaponCapacity+1), "page_2.html", "weapons", weaponCapacity + 1, 2}, + {"10 spells on page3", "page_3.html", "spells", 10, (10 + spellCapacity - 1) / spellCapacity}, // Dynamic calculation + {fmt.Sprintf("%d spells on page3", spellCapacity), "page_3.html", "spells", spellCapacity, 1}, + {fmt.Sprintf("%d spells on page3", spellCapacity+1), "page_3.html", "spells", spellCapacity + 1, 2}, } for _, tc := range testCases { @@ -562,7 +870,7 @@ func TestPaginateSkills_EmptyList(t *testing.T) { skills := []SkillViewModel{} // Act - pages, err := paginator.PaginateSkills(skills, "page1_stats.html", "") + pages, err := paginator.PaginateSkills(skills, "page_1.html", "") // Assert if err != nil { diff --git a/backend/pdfrender/pagination_utils_test.go b/backend/pdfrender/pagination_utils_test.go index cc16d38..bf7e682 100644 --- a/backend/pdfrender/pagination_utils_test.go +++ b/backend/pdfrender/pagination_utils_test.go @@ -11,27 +11,27 @@ func TestGenerateContinuationTemplateName(t *testing.T) { }{ { name: "First page returns original", - template: "page1_stats.html", + template: "page_1.html", pageNum: 1, - expected: "page1_stats.html", + expected: "page_1.html", }, { name: "Second page gets continuation name", - template: "page1_stats.html", + template: "page_1.html", pageNum: 2, - expected: "page1.2_stats.html", + expected: "page_1.2.html", }, { name: "Third page uses same continuation template (.2)", - template: "page2_play.html", + template: "page_2.html", pageNum: 3, - expected: "page2.2_play.html", + expected: "page_2.2.html", }, { name: "All continuation pages use .2 template", - template: "page1_stats.html", + template: "page_1.html", pageNum: 10, - expected: "page1.2_stats.html", + expected: "page_1.2.html", }, } @@ -53,23 +53,23 @@ func TestExtractBaseTemplateName(t *testing.T) { }{ { name: "Base template returns itself", - template: "page1_stats.html", - expected: "page1_stats.html", + template: "page_1.html", + expected: "page_1.html", }, { name: "Continuation template returns base", - template: "page1.2_stats.html", - expected: "page1_stats.html", + template: "page_1.2.html", + expected: "page_1.html", }, { name: "Multiple digit continuation", - template: "page1.10_stats.html", - expected: "page1_stats.html", + template: "page_1.10.html", + expected: "page_1.html", }, { name: "Different page type", - template: "page2.3_play.html", - expected: "page2_play.html", + template: "page_2.3.html", + expected: "page_2.html", }, { name: "Template without underscore", @@ -90,7 +90,7 @@ func TestExtractBaseTemplateName(t *testing.T) { func TestContinuationRoundTrip(t *testing.T) { // Test that generating a continuation name and extracting the base works correctly - original := "page1_stats.html" + original := "page_1.html" for pageNum := 1; pageNum <= 5; pageNum++ { continuation := GenerateContinuationTemplateName(original, pageNum) diff --git a/backend/pdfrender/render_with_continuation.go b/backend/pdfrender/render_with_continuation.go index f265ce0..5c806ad 100644 --- a/backend/pdfrender/render_with_continuation.go +++ b/backend/pdfrender/render_with_continuation.go @@ -20,53 +20,23 @@ func RenderPageWithContinuations( templateSet := DefaultA4QuerTemplateSet() paginator := NewPaginator(templateSet) - // Determine which list type this template handles - var distributions []PageDistribution - var err error + // Build data map from view model + dataMap := map[string]interface{}{ + "skills": viewModel.Skills, + "weapons": viewModel.Weapons, + "spells": viewModel.Spells, + "equipment": viewModel.Equipment, + "magicItems": viewModel.MagicItems, + } - switch templateName { - case "page1_stats.html": - // Paginate skills - distributions, err = paginator.PaginateSkills(viewModel.Skills, templateName, "") - if err != nil { - return nil, fmt.Errorf("failed to paginate skills: %w", err) - } + // Use unified pagination for all templates + distributions, err := paginator.PaginateMultiList(dataMap, templateName) + if err != nil { + return nil, fmt.Errorf("failed to paginate: %w", err) + } - case "page2_play.html": - // Page 2 has both skills and weapons that overflow together - // Use multi-list pagination so remaining items from both lists go to continuation pages - distributions, err = paginator.PaginatePage2PlayLists(viewModel.Skills, viewModel.Weapons, templateName) - if err != nil { - return nil, fmt.Errorf("failed to paginate page2 lists: %w", err) - } - - case "page3_spell.html": - // Paginate spells - distributions, err = paginator.PaginateSpells(viewModel.Spells, templateName) - if err != nil { - return nil, fmt.Errorf("failed to paginate spells: %w", err) - } - - case "page4_equip.html": - // Page 4 has a complex container-based layout where items are grouped by containers. - // The template expects the full equipment list to properly render containers and their contents. - // Pagination doesn't make sense here - render as single page with all equipment. - pageData, err := PreparePaginatedPageData(viewModel, templateName, startPageNumber, date) - if err != nil { - return nil, err - } - html, err := loader.RenderTemplateWithInlinedResources(templateName, pageData) - if err != nil { - return nil, err - } - pdf, err := renderer.RenderHTMLToPDF(html) - if err != nil { - return nil, err - } - return [][]byte{pdf}, nil - - default: - // For unknown templates, render single page without pagination + // If no distributions (empty data), render single empty page + if len(distributions) == 0 { pageData, err := PreparePaginatedPageData(viewModel, templateName, startPageNumber, date) if err != nil { return nil, err @@ -82,24 +52,7 @@ func RenderPageWithContinuations( return [][]byte{pdf}, nil } - // If only one page, use the simplified approach - if len(distributions) == 1 { - pageData, err := PreparePaginatedPageData(viewModel, templateName, startPageNumber, date) - if err != nil { - return nil, err - } - html, err := loader.RenderTemplateWithInlinedResources(templateName, pageData) - if err != nil { - return nil, err - } - pdf, err := renderer.RenderHTMLToPDF(html) - if err != nil { - return nil, err - } - return [][]byte{pdf}, nil - } - - // Multiple pages needed - render each one + // Render each distributed page for i, dist := range distributions { pageData := &PageData{ Character: viewModel.Character, @@ -112,58 +65,10 @@ func RenderPageWithContinuations( }, } - // Populate the page data based on the distribution - switch templateName { - case "page1_stats.html": - // Extract skills from distribution - if col1, ok := dist.Data["skills_column1"].([]SkillViewModel); ok { - pageData.SkillsColumn1 = col1 - } - if col2, ok := dist.Data["skills_column2"].([]SkillViewModel); ok { - pageData.SkillsColumn2 = col2 - } - // Combine for backward compatibility - pageData.Skills = append(pageData.SkillsColumn1, pageData.SkillsColumn2...) + // Populate page data from distribution + populatePageDataFromDistribution(pageData, dist) - case "page2_play.html": - // Extract all lists from distribution (skills and weapons) - if weapons, ok := dist.Data["weapons_main"].([]WeaponViewModel); ok { - pageData.Weapons = weapons - } - if learned, ok := dist.Data["skills_learned"].([]SkillViewModel); ok { - pageData.SkillsLearned = learned - } - if unlearned, ok := dist.Data["skills_unlearned"].([]SkillViewModel); ok { - // Unlearned skills are typically shown via general skills list - // Add to Skills for template compatibility - pageData.Skills = append(pageData.Skills, unlearned...) - } - if languages, ok := dist.Data["skills_languages"].([]SkillViewModel); ok { - pageData.SkillsLanguage = languages - } - - case "page3_spell.html": - // Extract spells from distribution - if left, ok := dist.Data["spells_left"].([]SpellViewModel); ok { - pageData.SpellsLeft = left - } - if right, ok := dist.Data["spells_right"].([]SpellViewModel); ok { - pageData.SpellsRight = right - } - // Combine for backward compatibility - pageData.Spells = append(pageData.SpellsLeft, pageData.SpellsRight...) - - case "page4_equip.html": - // Extract equipment from distribution - if equipment, ok := dist.Data["equipment_worn"].([]EquipmentViewModel); ok { - pageData.Equipment = append(pageData.Equipment, equipment...) - } - if equipment, ok := dist.Data["equipment_carried"].([]EquipmentViewModel); ok { - pageData.Equipment = append(pageData.Equipment, equipment...) - } - } - - // Render the page (use continuation template name if needed) + // Render the page html, err := loader.RenderTemplateWithInlinedResources(dist.TemplateName, pageData) if err != nil { return nil, fmt.Errorf("failed to render %s: %w", dist.TemplateName, err) @@ -180,6 +85,84 @@ func RenderPageWithContinuations( return pdfs, nil } +// populatePageDataFromDistribution populates PageData from a distribution +// This replaces the hardcoded switch statements for each template type +func populatePageDataFromDistribution(pageData *PageData, dist PageDistribution) { + // Populate data based on block names in distribution + for blockName, data := range dist.Data { + switch blockName { + // Skills blocks + case "skills_column1": + if skills, ok := data.([]SkillViewModel); ok { + pageData.SkillsColumn1 = skills + pageData.Skills = append(pageData.Skills, skills...) + } + case "skills_column2": + if skills, ok := data.([]SkillViewModel); ok { + pageData.SkillsColumn2 = skills + pageData.Skills = append(pageData.Skills, skills...) + } + case "skills_column3": + if skills, ok := data.([]SkillViewModel); ok { + pageData.SkillsColumn3 = skills + pageData.Skills = append(pageData.Skills, skills...) + } + case "skills_column4": + if skills, ok := data.([]SkillViewModel); ok { + pageData.SkillsColumn4 = skills + pageData.Skills = append(pageData.Skills, skills...) + } + case "skills_learned": + if skills, ok := data.([]SkillViewModel); ok { + pageData.SkillsLearned = skills + } + case "skills_unlearned": + if skills, ok := data.([]SkillViewModel); ok { + // Add to general Skills list for template compatibility + pageData.Skills = append(pageData.Skills, skills...) + } + case "skills_languages": + if skills, ok := data.([]SkillViewModel); ok { + pageData.SkillsLanguage = skills + } + + // Weapons blocks + case "weapons_main": + if weapons, ok := data.([]WeaponViewModel); ok { + pageData.Weapons = weapons + } + + // Spells blocks + case "spells_left": + if spells, ok := data.([]SpellViewModel); ok { + pageData.SpellsLeft = spells + pageData.Spells = append(pageData.Spells, spells...) + } + case "spells_right": + if spells, ok := data.([]SpellViewModel); ok { + pageData.SpellsRight = spells + pageData.Spells = append(pageData.Spells, spells...) + } + + // Equipment blocks + case "equipment_worn": + if equipment, ok := data.([]EquipmentViewModel); ok { + pageData.Equipment = append(pageData.Equipment, equipment...) + } + case "equipment_carried": + if equipment, ok := data.([]EquipmentViewModel); ok { + pageData.Equipment = append(pageData.Equipment, equipment...) + } + + // Magic items + case "magic_items": + if items, ok := data.([]MagicItemViewModel); ok { + pageData.MagicItems = items + } + } + } +} + // MergePDFs merges multiple PDF byte slices into a single PDF func MergePDFs(pdfList [][]byte, outputPath string) error { if len(pdfList) == 0 { diff --git a/backend/pdfrender/render_with_continuation_test.go b/backend/pdfrender/render_with_continuation_test.go index 4e520de..62ad85e 100644 --- a/backend/pdfrender/render_with_continuation_test.go +++ b/backend/pdfrender/render_with_continuation_test.go @@ -61,7 +61,7 @@ func TestRenderWithContinuations_SkillsOverflow(t *testing.T) { // Act - Render page1 with continuations pdfs, err := RenderPageWithContinuations( viewModel, - "page1_stats.html", + "page_1.html", 1, "20.12.2025", loader, @@ -77,7 +77,7 @@ func TestRenderWithContinuations_SkillsOverflow(t *testing.T) { templateSet := DefaultA4QuerTemplateSet() var skillsCapacity int for _, tmpl := range templateSet.Templates { - if tmpl.Metadata.Name == "page1_stats.html" { + if tmpl.Metadata.Name == "page_1.html" { for _, block := range tmpl.Metadata.Blocks { if block.ListType == "skills" { skillsCapacity += block.MaxItems @@ -163,7 +163,7 @@ func TestRenderWithContinuations_NoOverflow(t *testing.T) { // Act pdfs, err := RenderPageWithContinuations( viewModel, - "page1_stats.html", + "page_1.html", 1, "20.12.2025", loader, diff --git a/backend/pdfrender/template_metadata.go b/backend/pdfrender/template_metadata.go index 6697d0c..8ae2834 100644 --- a/backend/pdfrender/template_metadata.go +++ b/backend/pdfrender/template_metadata.go @@ -7,7 +7,7 @@ import ( // TemplateMetadata contains information about a template's capacity and requirements type TemplateMetadata struct { - Name string // Template name (e.g., "page1_stats.html") + Name string // Template name (e.g., "page_1.html") PageType string // "stats", "play", "spell", "equip" Description string Blocks []BlockMetadata // List blocks and their capacities @@ -49,10 +49,10 @@ func LoadTemplateSetFromFiles(templateDir string) (TemplateSet, error) { pageType string description string }{ - {"page1_stats.html", "stats", "Statistikseite mit Grundwerten"}, - {"page2_play.html", "play", "Spielbogen mit gelernten Fertigkeiten und Waffen"}, - {"page3_spell.html", "spell", "Zauberseite mit Zauberliste"}, - {"page4_equip.html", "equip", "Ausrüstungsseite"}, + {"page_1.html", "stats", "Statistikseite mit Grundwerten"}, + {"page_2.html", "play", "Spielbogen mit gelernten Fertigkeiten und Waffen"}, + {"page_3.html", "spell", "Zauberseite mit Zauberliste"}, + {"page_4.html", "equip", "Ausrüstungsseite"}, } // Load each template file and parse its metadata @@ -106,7 +106,7 @@ func getHardcodedTemplateSet() TemplateSet { Templates: []TemplateWithMeta{ { Metadata: TemplateMetadata{ - Name: "page1_stats.html", + Name: "page_1.html", PageType: "stats", Description: "Statistikseite mit Grundwerten", Blocks: []BlockMetadata{ @@ -124,11 +124,11 @@ func getHardcodedTemplateSet() TemplateSet { }, }, }, - Path: "templates/Default_A4_Quer/page1_stats.html", + Path: "templates/Default_A4_Quer/page_1.html", }, { Metadata: TemplateMetadata{ - Name: "page2_play.html", + Name: "page_2.html", PageType: "play", Description: "Spielbogen mit gelernten Fertigkeiten und Waffen", Blocks: []BlockMetadata{ @@ -157,11 +157,11 @@ func getHardcodedTemplateSet() TemplateSet { }, }, }, - Path: "templates/Default_A4_Quer/page2_play.html", + Path: "templates/Default_A4_Quer/page_2.html", }, { Metadata: TemplateMetadata{ - Name: "page3_spell.html", + Name: "page_3.html", PageType: "spell", Description: "Zauberseite mit Zauberliste", Blocks: []BlockMetadata{ @@ -184,11 +184,11 @@ func getHardcodedTemplateSet() TemplateSet { }, }, }, - Path: "templates/Default_A4_Quer/page3_spell.html", + Path: "templates/Default_A4_Quer/page_3.html", }, { Metadata: TemplateMetadata{ - Name: "page4_equip.html", + Name: "page_4.html", PageType: "equip", Description: "Ausrüstungsseite", Blocks: []BlockMetadata{ @@ -204,7 +204,7 @@ func getHardcodedTemplateSet() TemplateSet { }, }, }, - Path: "templates/Default_A4_Quer/page4_equip.html", + Path: "templates/Default_A4_Quer/page_4.html", }, }, } diff --git a/backend/pdfrender/template_metadata_loader_test.go b/backend/pdfrender/template_metadata_loader_test.go index 198df7b..d2113e5 100644 --- a/backend/pdfrender/template_metadata_loader_test.go +++ b/backend/pdfrender/template_metadata_loader_test.go @@ -17,17 +17,17 @@ func TestLoadTemplateSetFromFiles(t *testing.T) { t.Fatal("Expected templates, got none") } - // Find page1_stats.html and verify its metadata matches the HTML comments + // Find page_1.html and verify its metadata matches the HTML comments var page1 *TemplateWithMeta for i := range templateSet.Templates { - if templateSet.Templates[i].Metadata.Name == "page1_stats.html" { + if templateSet.Templates[i].Metadata.Name == "page_1.html" { page1 = &templateSet.Templates[i] break } } if page1 == nil { - t.Fatal("page1_stats.html not found in template set") + t.Fatal("page_1.html not found in template set") } // Check that blocks were parsed from HTML @@ -36,7 +36,7 @@ func TestLoadTemplateSetFromFiles(t *testing.T) { } // Verify skills_column1 block - read expected value directly from template file - templateContent, err := os.ReadFile("../templates/Default_A4_Quer/page1_stats.html") + templateContent, err := os.ReadFile("../templates/Default_A4_Quer/page_1.html") if err != nil { t.Fatalf("Failed to read template file: %v", err) } @@ -85,7 +85,7 @@ func TestDefaultA4QuerTemplateSet_LoadsFromFiles(t *testing.T) { // Verify metadata comes from template files, not hardcoded // Read expected value directly from template file - templateContent, err := os.ReadFile("../templates/Default_A4_Quer/page3_spell.html") + templateContent, err := os.ReadFile("../templates/Default_A4_Quer/page_3.html") if err != nil { t.Fatalf("Failed to read template file: %v", err) } @@ -105,14 +105,14 @@ func TestDefaultA4QuerTemplateSet_LoadsFromFiles(t *testing.T) { var page3 *TemplateWithMeta for i := range templateSet.Templates { - if templateSet.Templates[i].Metadata.Name == "page3_spell.html" { + if templateSet.Templates[i].Metadata.Name == "page_3.html" { page3 = &templateSet.Templates[i] break } } if page3 == nil { - t.Fatal("page3_spell.html not found") + t.Fatal("page_3.html not found") } var spellsLeft *BlockMetadata diff --git a/backend/pdfrender/templates.go b/backend/pdfrender/templates.go index c67c677..7e77b02 100644 --- a/backend/pdfrender/templates.go +++ b/backend/pdfrender/templates.go @@ -68,8 +68,8 @@ func (tl *TemplateLoader) LoadTemplates() error { } // RenderTemplate renders a specific template with the given data -// For continuation templates (e.g., "page1.2_stats.html"), it automatically -// falls back to the base template (e.g., "page1_stats.html") if the continuation +// For continuation templates (e.g., "page_1.2.html"), it automatically +// falls back to the base template (e.g., "page_1.html") if the continuation // template doesn't exist as a physical file func (tl *TemplateLoader) RenderTemplate(templateName string, data interface{}) (string, error) { if tl.templates == nil { @@ -79,7 +79,7 @@ func (tl *TemplateLoader) RenderTemplate(templateName string, data interface{}) tmpl := tl.templates.Lookup(templateName) if tmpl == nil { // Try to extract base template name for continuation pages - // e.g., "page1.2_stats.html" -> "page1_stats.html" + // e.g., "page_1.2.html" -> "page_1.html" baseTemplateName := ExtractBaseTemplateName(templateName) if baseTemplateName != templateName { tmpl = tl.templates.Lookup(baseTemplateName) diff --git a/backend/pdfrender/templates_test.go b/backend/pdfrender/templates_test.go index 1af7f2b..73ec6d8 100644 --- a/backend/pdfrender/templates_test.go +++ b/backend/pdfrender/templates_test.go @@ -46,7 +46,7 @@ func TestRenderTemplate_BasicData(t *testing.T) { } // Act - html, err := loader.RenderTemplate("page1_stats.html", data) + html, err := loader.RenderTemplate("page_1.html", data) // Assert if err != nil { @@ -75,7 +75,7 @@ func TestGetTemplateMetadata(t *testing.T) { } // Act - metadata := loader.GetTemplateMetadata("page3_spell.html") + metadata := loader.GetTemplateMetadata("page_3.html") // Assert if len(metadata) == 0 { @@ -86,13 +86,13 @@ func TestGetTemplateMetadata(t *testing.T) { templateSet := DefaultA4QuerTemplateSet() var page3Template *TemplateWithMeta for i := range templateSet.Templates { - if templateSet.Templates[i].Metadata.Name == "page3_spell.html" { + if templateSet.Templates[i].Metadata.Name == "page_3.html" { page3Template = &templateSet.Templates[i] break } } if page3Template == nil { - t.Fatal("page3_spell.html template not found") + t.Fatal("page_3.html template not found") } // Get expected values from template @@ -150,7 +150,7 @@ func TestRenderTemplate_WithSkills(t *testing.T) { } // Act - html, err := loader.RenderTemplate("page1_stats.html", data) + html, err := loader.RenderTemplate("page_1.html", data) // Assert if err != nil { diff --git a/backend/pdfrender/todo.md b/backend/pdfrender/todo.md index d10a56e..2e446e7 100644 --- a/backend/pdfrender/todo.md +++ b/backend/pdfrender/todo.md @@ -1,102 +1,11 @@ -## COMPLETED - -* ✅ Page 2 Weapons Table now shows the following information: - - For each Weapon a character has: - - Its name - - ✅ the Fertigkeitswert (EW): Waffenfertigkeit.Fertigkeitswert + Character.AngriffBonus + Weapon.Anb (if equipped) - - ✅ the Schaden (Damage) including character's SchadenBonus and weapon's Schadensbonus - - ✅ If it is a ranged weapon, the ranges for near, medium and far - - Implementation notes: - - ✅ AngriffBonus and SchadenBonus are now calculated in DerivedValueSet from character attributes - - ✅ mapWeapons() function now: - - Calculates EW = Waffenfertigkeit.Fertigkeitswert + Character.AngriffBonus + Weapon.Anb - - Matches Waffenfertigkeiten with equipped Waffen by name - - Adds weapon attack bonus (Anb) if weapon is equipped - - ✅ Added test TestMapWeapons_WithEWCalculation to verify correct EW calculation - - ✅ Used TDD approach: wrote failing test first, then implemented solution - -* ✅ Template MaxItems expectations are now dynamic: - - Tests now read MaxItems values directly from template HTML comments - - Updated TestLoadTemplateSetFromFiles to parse templates dynamically - - Updated TestDefaultA4QuerTemplateSet_LoadsFromFiles to use dynamic expectations - - Updated TestPaginationUsesTemplateMetadata to read from template files - - Tests will automatically adapt when template capacities change - -* ✅ Weapon model enhancements: - - ✅ Added RangeNear, RangeMiddle, RangeFar integer fields to gsm_weapons table - - ✅ Added IsRanged() method that returns true if at least one range value > 0 - - ✅ Damage field already exists in gsm_weapons table - - ✅ EqWaffe already contains bonus values: Anb (Attack), Schb (Damage), Abwb (Defense) - - ✅ All tests pass with new fields - -* ✅ Page 2 Weapons Table - Complete implementation: - - ✅ Changed TestVisualInspection_AllPages to load character Fanjo Vetrani (ID 18) from test database - - ✅ Damage calculation implemented: - - Calculates total damage: Base Weapon Damage + Character SchadenBonus + Weapon Schb - - Format: "1W6+3" where +3 = SchadenBonus + Schb - - Implemented calculateWeaponDamage() function - - Added TestMapWeapons_WithDamageCalculation test - - ✅ Ranged weapon ranges implemented: - - Shows ranges for ranged weapons (Bogen, Armbrust, etc.) - - Format: "Nah/Mittel/Fern" (e.g., "10/30/100") - - Implemented calculateWeaponRange() function - - Added TestMapWeapons_WithRangedWeaponRanges test - - Marks weapons as ranged using IsRanged field - - ✅ All tests pass - -* ✅ Continuation pages for overflow items: - - When items exceed template capacity, continuation pages are automatically created - - Continuation pages follow naming pattern: page1.2_stats.html, page1.3_stats.html, etc. - - Template loader automatically falls back to base template for continuation pages - - No physical continuation template files needed - reuses base template structure - - **NEW: RenderPageWithContinuations() function generates actual PDF files** - - Each continuation page is rendered as a separate PDF - - PDFs can be merged into a single combined file - - Implemented using TDD: - - Created comprehensive tests in continuation_test.go - - Added GenerateContinuationTemplateName() function - - Added ExtractBaseTemplateName() function - - Updated paginateList() to generate continuation template names - - Updated RenderTemplate() to handle continuation template fallback - - **Created RenderPageWithContinuations() to actually render multiple PDFs** - - **Created integration test that saves real PDF files to disk** - - All existing tests updated to work with dynamic template capacities - - Fully tested and working end-to-end - - **VERIFIED: 5 continuation pages generated for 50 skills, saved to /tmp/bamort_continuation_test/** - - -* ✅ 1. create API endpoint for listing available export templates - * Endpoint: GET /api/pdf/templates - * Returns: JSON array of TemplateInfo objects [{id, name, description}] - * Test: TestListTemplates passes - * Configuration: Uses config.Cfg.TemplatesDir (default: "./templates") -* ✅ 2. create API endpoint for exporting character to PDF. - * Endpoint: GET /api/pdf/export/:id?template=xxx&showUserName=true - * Endpoint takes parameter "template", "show user name". - * Return combined PDF file for download or display in Browser - * Renders all 4 pages (stats, play, spells, equipment) with continuation pages - * Merges all PDFs into single combined file - * Returns PDF with proper headers: Content-Type: application/pdf, Content-Disposition - * Tests: TestExportCharacterToPDF, TestExportCharacterToPDF_WithTemplate, TestExportCharacterToPDF_CharacterNotFound all pass - * Configuration: Uses config.Cfg.TemplatesDir for template path resolution - * Status: ✅ Deployed and running in Docker container, verified with logs -* ✅ 3. create exporting function in Frontend - * ✅ The UI element to start the export function is to the left side of the character's name (CharacterDetails.vue) - * ✅ Template selection dropdown implemented - auto-selects first template - * ✅ Export button with loading state (disabled while exporting) - * ✅ PDF opens in new browser tab using window.open() - * ✅ Translations added for German and English - * ✅ API integration: Fetches templates on component load, calls export endpoint with selected template - * ✅ Error handling with user-friendly alerts - * Status: ✅ Deployed with HMR, ready for testing - ## TODO (Remaining) -* 1. create a directory xporttemp in the backend -* 2. save the PDF to file during the ExportCharacterToPDF call and return the filename charname+timestamp.pdf. Make shure the filename contains no spaces or special chars that might disturb the download -* 3. create an API endpoint to load the file from xporttemp -* 4. create a maintenance endpoint to clean up the xporttemp directory. remove all files that are older than 7 days -* 5. change the frontend to get the PDF from the new API endpoint +* 1. in RenderPageWithContinuations use a switch to select a templatename and the ncall a paginator function that is designed explicitly for this template page with the current content. This will not work when we add more and different templates. We nned a solution where the code can find out which data is present on the template page and if these datastructures are to be continued on a continuation page or on the next regular page in row. it needs to find out how many elements can be rendered into the page or it's continuation page. It needs to handle if there are multiple datastructures present on one page. +for this to be accomplished we need some kind of template metadata or inline datastructure(Block) configuration. +I want to change the templateName from page1_stats.html to page_1.html and accordingly the continuation page from page1.2_stats.html to page_1.2.html this makes clear that a page is not tight to one or another datastructure. +Page 4 has a complex container-based layout we leave this out for the moment. +Plan a refacturation of PaginateSkills, PaginatePage2PlayLists and PaginateSpells into one unified function. + + ### Later * continuation of lists does not work as expected but good enough for a first shot diff --git a/backend/pdfrender/todo_fixes_test.go b/backend/pdfrender/todo_fixes_test.go index 4071080..baa8caf 100644 --- a/backend/pdfrender/todo_fixes_test.go +++ b/backend/pdfrender/todo_fixes_test.go @@ -10,7 +10,7 @@ import ( // TestPaginationUsesTemplateMetadata verifies tests use actual template MAX values func TestPaginationUsesTemplateMetadata(t *testing.T) { // Read expected values directly from template file - templateContent, err := os.ReadFile("../templates/Default_A4_Quer/page2_play.html") + templateContent, err := os.ReadFile("../templates/Default_A4_Quer/page_2.html") if err != nil { t.Fatalf("Failed to read template file: %v", err) } @@ -26,14 +26,14 @@ func TestPaginationUsesTemplateMetadata(t *testing.T) { // Find page2 var page2 *TemplateWithMeta for i := range templateSet.Templates { - if templateSet.Templates[i].Metadata.Name == "page2_play.html" { + if templateSet.Templates[i].Metadata.Name == "page_2.html" { page2 = &templateSet.Templates[i] break } } if page2 == nil { - t.Fatal("page2_play.html not found") + t.Fatal("page_2.html not found") } // Verify blocks exist and have correct MAX from template @@ -64,7 +64,7 @@ func TestPaginationUsesTemplateMetadata(t *testing.T) { func TestPage2PaginationWithCorrectCapacities(t *testing.T) { // Read expected values directly from template file - templateContent, err := os.ReadFile("../templates/Default_A4_Quer/page2_play.html") + templateContent, err := os.ReadFile("../templates/Default_A4_Quer/page_2.html") if err != nil { t.Fatalf("Failed to read template file: %v", err) } @@ -88,7 +88,7 @@ func TestPage2PaginationWithCorrectCapacities(t *testing.T) { }, } - pageData, err := PreparePaginatedPageData(viewModel, "page2_play.html", 2, "2024-01-01") + pageData, err := PreparePaginatedPageData(viewModel, "page_2.html", 2, "2024-01-01") if err != nil { t.Fatalf("Failed to prepare page data: %v", err) } @@ -109,7 +109,7 @@ func TestPage2PaginationWithCorrectCapacities(t *testing.T) { func TestPage3MagicItemsCapacity(t *testing.T) { // Read expected values directly from template file - templateContent, err := os.ReadFile("../templates/Default_A4_Quer/page3_spell.html") + templateContent, err := os.ReadFile("../templates/Default_A4_Quer/page_3.html") if err != nil { t.Fatalf("Failed to read template file: %v", err) } @@ -128,7 +128,7 @@ func TestPage3MagicItemsCapacity(t *testing.T) { }, } - pageData, err := PreparePaginatedPageData(viewModel, "page3_spell.html", 3, "2024-01-01") + pageData, err := PreparePaginatedPageData(viewModel, "page_3.html", 3, "2024-01-01") if err != nil { t.Fatalf("Failed to prepare page data: %v", err) } diff --git a/backend/pdfrender/viewmodel.go b/backend/pdfrender/viewmodel.go index e74ec21..ef7c9e4 100644 --- a/backend/pdfrender/viewmodel.go +++ b/backend/pdfrender/viewmodel.go @@ -185,14 +185,16 @@ type PageData struct { // Lists sliced according to template block metadata Skills []SkillViewModel - SkillsColumn1 []SkillViewModel // For two-column skill layout (page1) - SkillsColumn2 []SkillViewModel // For two-column skill layout (page1) - SkillsLearned []SkillViewModel // Filtered learned skills (page2) - SkillsLanguage []SkillViewModel // Filtered language skills (page2) + SkillsColumn1 []SkillViewModel // For two-column skill layout (page_1) + SkillsColumn2 []SkillViewModel // For two-column skill layout (page_1) + SkillsColumn3 []SkillViewModel // For continuation pages (page_1.2) + SkillsColumn4 []SkillViewModel // For continuation pages (page_1.2) + SkillsLearned []SkillViewModel // Filtered learned skills (page_2) + SkillsLanguage []SkillViewModel // Filtered language skills (page_2) Weapons []WeaponViewModel Spells []SpellViewModel - SpellsLeft []SpellViewModel // Left column spells (page3) - SpellsRight []SpellViewModel // Right column spells (page3) + SpellsLeft []SpellViewModel // Left column spells (page_3) + SpellsRight []SpellViewModel // Right column spells (page_3) MagicItems []MagicItemViewModel Equipment []EquipmentViewModel GameResults []GameResultViewModel diff --git a/backend/templates/Default_A4_Quer/page1.2_stats.html b/backend/templates/Default_A4_Quer/page_1.2.html similarity index 100% rename from backend/templates/Default_A4_Quer/page1.2_stats.html rename to backend/templates/Default_A4_Quer/page_1.2.html diff --git a/backend/templates/Default_A4_Quer/page1_stats.html b/backend/templates/Default_A4_Quer/page_1.html similarity index 100% rename from backend/templates/Default_A4_Quer/page1_stats.html rename to backend/templates/Default_A4_Quer/page_1.html diff --git a/backend/templates/Default_A4_Quer/page2.2_play.html b/backend/templates/Default_A4_Quer/page_2.2.html similarity index 100% rename from backend/templates/Default_A4_Quer/page2.2_play.html rename to backend/templates/Default_A4_Quer/page_2.2.html diff --git a/backend/templates/Default_A4_Quer/page2_play.html b/backend/templates/Default_A4_Quer/page_2.html similarity index 100% rename from backend/templates/Default_A4_Quer/page2_play.html rename to backend/templates/Default_A4_Quer/page_2.html diff --git a/backend/templates/Default_A4_Quer/page3.2_spell.html b/backend/templates/Default_A4_Quer/page_3.2.html similarity index 100% rename from backend/templates/Default_A4_Quer/page3.2_spell.html rename to backend/templates/Default_A4_Quer/page_3.2.html diff --git a/backend/templates/Default_A4_Quer/page3_spell.html b/backend/templates/Default_A4_Quer/page_3.html similarity index 100% rename from backend/templates/Default_A4_Quer/page3_spell.html rename to backend/templates/Default_A4_Quer/page_3.html diff --git a/backend/templates/Default_A4_Quer/page4.2_equip.html b/backend/templates/Default_A4_Quer/page_4.2.html similarity index 100% rename from backend/templates/Default_A4_Quer/page4.2_equip.html rename to backend/templates/Default_A4_Quer/page_4.2.html diff --git a/backend/templates/Default_A4_Quer/page4_equip.html b/backend/templates/Default_A4_Quer/page_4.html similarity index 100% rename from backend/templates/Default_A4_Quer/page4_equip.html rename to backend/templates/Default_A4_Quer/page_4.html diff --git a/docker/Dockerfile.backend.dev b/docker/Dockerfile.backend.dev index 81f1d0d..2daab5e 100644 --- a/docker/Dockerfile.backend.dev +++ b/docker/Dockerfile.backend.dev @@ -1,5 +1,4 @@ # Development Dockerfile für Go Backend mit Live-Reloading -#FROM golang:1.23-alpine FROM golang:1.25-alpine # Install necessary packages for CGO and SQLite