382 lines
No EOL
16 KiB
C#
382 lines
No EOL
16 KiB
C#
using System;
|
|
using System.Collections.Generic;
|
|
using System.Data;
|
|
using System.IO;
|
|
using System.Linq;
|
|
using Dapper;
|
|
using Microsoft.Data.SqlClient;
|
|
using QuestPDF.Fluent;
|
|
using QuestPDF.Helpers;
|
|
using QuestPDF.Infrastructure;
|
|
|
|
namespace QuizTools
|
|
{
|
|
internal class Program
|
|
{
|
|
const int MaxQuestionImageHeight = 60;
|
|
const int MaxAnswerImageHeight = 40;
|
|
static void Main(string[] args)
|
|
{
|
|
// 1. License Configuration
|
|
QuestPDF.Settings.License = LicenseType.Community;
|
|
|
|
string connectionString = "Server=localhost;Database=idpbw;Trusted_Connection=True;TrustServerCertificate=True;";
|
|
|
|
Console.WriteLine("Fetching data from SQL Server...");
|
|
var modules = FetchQuizData(connectionString);
|
|
|
|
Console.WriteLine("Generating PDF Booklet...");
|
|
GeneratePdfBooklet(modules, "Quiz_Review_Booklet.pdf");
|
|
|
|
Console.WriteLine("PDF generated successfully!");
|
|
}
|
|
|
|
// --- Data Models ---
|
|
public class ModuleModel
|
|
{
|
|
public int Id { get; set; }
|
|
public string NameNL { get; set; } = string.Empty;
|
|
public string NameFR { get; set; } = string.Empty;
|
|
public string NameEN { get; set; } = string.Empty;
|
|
public List<QuestionModel> Questions { get; set; } = new();
|
|
}
|
|
|
|
public class QuestionModel
|
|
{
|
|
public int Id { get; set; }
|
|
public byte[]? Image { get; set; }
|
|
public Dictionary<string, string> Text { get; set; } = new();
|
|
public List<AnswerModel> Answers { get; set; } = new();
|
|
}
|
|
|
|
public class AnswerModel
|
|
{
|
|
public int Id { get; set; }
|
|
public bool IsCorrect { get; set; }
|
|
public byte[]? Image { get; set; }
|
|
public Dictionary<string, string> Text { get; set; } = new();
|
|
}
|
|
|
|
// --- Database Fetching ---
|
|
static List<ModuleModel> FetchQuizData(string connStr)
|
|
{
|
|
using IDbConnection db = new SqlConnection(connStr);
|
|
string query = @"
|
|
SELECT m.moduleId AS ModuleId, m.naamNL AS ModuleNameNL,m.naamFR AS ModuleNameFR, m.naamEN AS ModuleNameEN, q.vraagId AS QuestionId, q.NL AS QTextNL, q.FR AS QTextFR, q.EN AS QTextEN, q_img.Image AS QImage, a.antwId AS AnswerId, a.NL AS ATextNL, a.FR AS ATextFR, a.EN AS ATextEN, a.correct AS IsCorrect, a_img.Image AS AImage
|
|
FROM dbo.modules m
|
|
JOIN quiz.vragen q ON m.moduleId=q.moduleId
|
|
LEFT JOIN dbo.QuizImages q_img ON q.QuizImageId=q_img.QuizImageId
|
|
JOIN quiz.antwoorden a ON q.vraagId=a.vraagId
|
|
LEFT JOIN dbo.QuizImages a_img ON a.QuizImageId=a_img.QuizImageId
|
|
WHERE q.active=1 AND q.deleted=0
|
|
ORDER BY m.moduleId, q.vraagId, a.antwId;
|
|
";
|
|
|
|
var rows = db.Query(query);
|
|
var modulesDict = new Dictionary<int, ModuleModel>();
|
|
|
|
foreach (var row in rows)
|
|
{
|
|
int modId = row.ModuleId;
|
|
if (!modulesDict.TryGetValue(modId, out var module))
|
|
{
|
|
module = new ModuleModel
|
|
{
|
|
Id = modId,
|
|
NameNL = row.ModuleNameNL ?? string.Empty,
|
|
NameFR = row.ModuleNameFR ?? string.Empty,
|
|
NameEN = row.ModuleNameEN ?? string.Empty
|
|
};
|
|
modulesDict[modId] = module;
|
|
}
|
|
|
|
int qId = row.QuestionId;
|
|
var question = module.Questions.FirstOrDefault(q => q.Id == qId);
|
|
if (question == null)
|
|
{
|
|
byte[]? qImage = row.QImage is DBNull ? null : (byte[]?)row.QImage;
|
|
if (qImage != null && qImage.Length <100) qImage = null;
|
|
question = new QuestionModel { Id = qId, Image = qImage };
|
|
module.Questions.Add(question);
|
|
}
|
|
question.Text["nl"] = row.QTextNL;
|
|
question.Text["fr"] = row.QTextFR;
|
|
question.Text["en"] = row.QTextEN;
|
|
|
|
int aId = row.AnswerId;
|
|
var answer = question.Answers.FirstOrDefault(a => a.Id == aId);
|
|
if (answer == null)
|
|
{
|
|
byte[]? aImage = row.AImage is DBNull ? null : (byte[]?)row.AImage;
|
|
if (aImage != null && aImage.Length < 100) aImage = null;
|
|
answer = new AnswerModel { Id = aId, IsCorrect = row.IsCorrect, Image = aImage };
|
|
question.Answers.Add(answer);
|
|
}
|
|
answer.Text["nl"] = row.ATextNL;
|
|
answer.Text["fr"] = row.ATextFR;
|
|
answer.Text["en"] = row.ATextEN;
|
|
}
|
|
|
|
return modulesDict.Values.ToList();
|
|
}
|
|
|
|
// --- PDF Generation Logic ---
|
|
static void GeneratePdfBooklet(List<ModuleModel> modules, string outputPath)
|
|
{
|
|
Document.Create(container =>
|
|
{
|
|
//container.WithSettings(settings =>
|
|
//{
|
|
// settings.PdfA = false; // Set to true if you need PDF/A compliance
|
|
//});
|
|
|
|
container.Page(page =>
|
|
{
|
|
page.Size(PageSizes.A4);
|
|
page.Margin(15, Unit.Millimetre);
|
|
page.DefaultTextStyle(x => x.FontFamily("Segoe UI").FontSize(9).FontColor("#1E293B"));
|
|
|
|
page.Header().Element(ComposeDocumentHeader);
|
|
|
|
page.Content().PaddingVertical(10).Column(column =>
|
|
{
|
|
bool isFirstModule = true;
|
|
|
|
foreach (var module in modules)
|
|
{
|
|
// Avoid inserting a blank page break before the very first cover page
|
|
if (!isFirstModule)
|
|
{
|
|
column.Item().PageBreak();
|
|
}
|
|
isFirstModule = false;
|
|
|
|
// 1. Render Full-Page Module Cover
|
|
column.Item().Element(c => ComposeModuleTitlePage(c, module));
|
|
|
|
// 2. Start Questions on a Fresh Page Right After Title
|
|
column.Item().PageBreak();
|
|
|
|
// 3. Render Question Cards for this Module
|
|
column.Item().Column(qCol =>
|
|
{
|
|
qCol.Spacing(15);
|
|
foreach (var question in module.Questions)
|
|
{
|
|
qCol.Item().Element(c => ComposeQuestionCard(c, question));
|
|
}
|
|
});
|
|
}
|
|
});
|
|
|
|
page.Footer().Element(ComposeDocumentFooter);
|
|
});
|
|
}).WithMetadata(new DocumentMetadata
|
|
{
|
|
Title = "Quiz Review Booklet",
|
|
Author = "LSW-Tech Luc Vandenbroucke",
|
|
Subject = "Multilingual Quiz Questions and Answers (NL / FR / EN)",
|
|
Keywords = "Quiz, HPH",
|
|
Creator = "QuizTools Application"
|
|
}).GeneratePdf(outputPath);
|
|
}
|
|
static void ComposeModuleTitlePage(IContainer container, ModuleModel module)
|
|
{
|
|
container.AlignCenter().AlignMiddle().Column(col =>
|
|
{
|
|
// Badge Header
|
|
col.Item().AlignCenter().Background("#1E293B").PaddingHorizontal(12).PaddingVertical(4)
|
|
.CornerRadius(4).Text($"MODULE {module.Id}").Bold().FontSize(12).FontColor("#FFFFFF");
|
|
|
|
// Module Names Stacked
|
|
col.Item().PaddingTop(20).AlignCenter().Text(module.NameNL)
|
|
.Bold().FontSize(22).FontColor("#0F172A");
|
|
|
|
col.Item().PaddingTop(6).AlignCenter().Text(module.NameFR)
|
|
.SemiBold().FontSize(18).FontColor("#3B82F6");
|
|
|
|
col.Item().PaddingTop(6).AlignCenter().Text(module.NameEN)
|
|
.Medium().FontSize(16).FontColor("#10B981");
|
|
|
|
// Decorative Accent Bar
|
|
col.Item().PaddingTop(25).Width(100).Height(3).Background("#CBD5E1");
|
|
|
|
// Question Counter
|
|
col.Item().PaddingTop(15).AlignCenter().Text($"{module.Questions.Count} Questions")
|
|
.FontSize(11).FontColor("#64748B");
|
|
});
|
|
}
|
|
|
|
// --- Header Component ---
|
|
static void ComposeDocumentHeader(IContainer container)
|
|
{
|
|
container.BorderBottom(1).BorderColor("#E2E8F0").PaddingBottom(5).Row(row =>
|
|
{
|
|
row.RelativeItem().Text("Quiz Questions and Answers").Bold().FontSize(10).FontColor("#1E293B");
|
|
row.AutoItem().Text("23/08/2026").FontSize(10).FontColor("#64748B");
|
|
});
|
|
}
|
|
|
|
// --- First-Page Title Banner Component ---
|
|
static void ComposeTitleBanner(IContainer container)
|
|
{
|
|
container.Background("#F8FAFC").Border(1).BorderColor("#CBD5E1").CornerRadius(4).Padding(12).Row(row =>
|
|
{
|
|
row.RelativeItem().Column(col =>
|
|
{
|
|
col.Item().Text("Quiz Questions and Answers").Bold().FontSize(18).FontColor("#0F172A");
|
|
col.Item().PaddingTop(2).Text("Multilingual Module Review Export (NL / FR / EN)").FontSize(10).FontColor("#64748B");
|
|
});
|
|
row.AutoItem().AlignBottom().Text("Date: 23/08/2026").FontSize(10).FontColor("#64748B");
|
|
});
|
|
}
|
|
|
|
// --- Footer Component ---
|
|
//static void ComposeDocumentFooter(IContainer container)
|
|
//{
|
|
// container.BorderTop(1).BorderColor("#E2E8F0").PaddingTop(5).Row(row =>
|
|
// {
|
|
// // Left side: Creator info & date
|
|
// row.RelativeItem().Text(x =>
|
|
// {
|
|
// x.Span("Created by ").FontColor("#64748B");
|
|
// x.Span("Luc Vandenbroucke").Bold().FontColor("#1E293B");
|
|
// x.Span(" — 23.08.2026").FontColor("#64748B");
|
|
// });
|
|
|
|
// // Right side: Dynamic Page Numbering
|
|
// row.AutoItem().Text(x =>
|
|
// {
|
|
// x.Span("Page ").FontColor("#64748B");
|
|
// x.CurrentPageNumber().FontColor("#1E293B").Bold();
|
|
// x.Span(" of ").FontColor("#64748B");
|
|
// x.TotalPages().FontColor("#1E293B").Bold();
|
|
// });
|
|
// });
|
|
//}
|
|
|
|
static string GetSvgContent(string filePath)
|
|
{
|
|
if (File.Exists(filePath))
|
|
{
|
|
return File.ReadAllText(filePath);
|
|
}
|
|
|
|
// Fallback minimal SVG dot if file is missing
|
|
return @"<svg height='20' width='20'><circle cx='10' cy='10' r='8' fill='#1E293B'/></svg>";
|
|
}
|
|
|
|
static void ComposeDocumentFooter(IContainer container)
|
|
{
|
|
// Read your SVG string from file or a string variable
|
|
string mySvg = GetSvgContent("images\\lswtech300-95.svg");
|
|
|
|
container.BorderTop(1).BorderColor("#E2E8F0").PaddingTop(5).Row(row =>
|
|
{
|
|
// Left side: Render SVG image (constrained in height)
|
|
row.RelativeItem().Row(leftRow =>
|
|
{
|
|
leftRow.AutoItem()
|
|
.MaxHeight(16) // Keeps the SVG small and aligned with footer text
|
|
.Svg(mySvg);
|
|
|
|
leftRow.AutoItem()
|
|
.PaddingLeft(6)
|
|
.AlignMiddle()
|
|
.Text("— 23.08.2026")
|
|
.FontSize(8)
|
|
.FontColor("#64748B");
|
|
});
|
|
|
|
// Right side: Page Numbering
|
|
row.AutoItem().AlignMiddle().Text(x =>
|
|
{
|
|
x.Span("Page ").FontColor("#64748B");
|
|
x.CurrentPageNumber().FontColor("#1E293B").Bold();
|
|
x.Span(" of ").FontColor("#64748B");
|
|
x.TotalPages().FontColor("#1E293B").Bold();
|
|
});
|
|
});
|
|
}
|
|
|
|
static void ComposeModuleHeader(IContainer container, string title)
|
|
{
|
|
container.BorderBottom(2).BorderColor("#1E293B").PaddingBottom(5).Text(title)
|
|
.FontSize(16).Bold().FontColor("#1E293B");
|
|
}
|
|
|
|
static void ComposeQuestionCard(IContainer container, QuestionModel q)
|
|
{
|
|
// NO .ShowEntire() on the root container so multi-page questions can render
|
|
container.ShowEntire().Border(1).BorderColor("#CBD5E1").CornerRadius(4).Column(column =>
|
|
{
|
|
// 1. Keep the card header together with the first text block if possible
|
|
column.Item().ShowEntire().Background("#1E293B").Padding(6)
|
|
.Text($"Question #{q.Id}").Bold().FontColor("#FFFFFF");
|
|
|
|
column.Item().Padding(10).Column(body =>
|
|
{
|
|
// Question Image
|
|
if (q.Image != null && q.Image.Length > 0)
|
|
{
|
|
body.Item().PaddingVertical(6).AlignCenter().MaxHeight(MaxQuestionImageHeight ).Image(q.Image);
|
|
}
|
|
|
|
// Language Text Blocks
|
|
ComposeLangBlock(body, "NL", q.Text.GetValueOrDefault("nl"), "#F97316");
|
|
ComposeLangBlock(body, "FR", q.Text.GetValueOrDefault("fr"), "#3B82F6");
|
|
ComposeLangBlock(body, "EN", q.Text.GetValueOrDefault("en"), "#10B981");
|
|
|
|
// Answers Section
|
|
body.Item().PaddingTop(8).Text("ANSWERS").Bold().FontSize(8).FontColor("#64748B");
|
|
|
|
foreach (var ans in q.Answers)
|
|
{
|
|
// Each individual answer option stays intact on one page
|
|
body.Item().PaddingTop(5).Element(c => ComposeAnswerCard(c, ans));
|
|
}
|
|
});
|
|
});
|
|
}
|
|
|
|
static void ComposeAnswerCard(IContainer container, AnswerModel a)
|
|
{
|
|
string borderColor = a.IsCorrect ? "#86EFAC" : "#E2E8F0";
|
|
string bgColor = a.IsCorrect ? "#F0FDF4" : "#FFFFFF";
|
|
|
|
// .ShowEntire() here prevents individual answer choices from splitting mid-text
|
|
container.ShowEntire().Border(1).BorderColor(borderColor).Background(bgColor).CornerRadius(4).Padding(6).Column(col =>
|
|
{
|
|
col.Item().Row(row =>
|
|
{
|
|
row.RelativeItem().Text($"Option {a.Id}").Bold();
|
|
if (a.IsCorrect)
|
|
{
|
|
row.AutoItem().Background("#16A34A").PaddingHorizontal(4).PaddingVertical(1)
|
|
.CornerRadius(2).Text("CORRECT").FontSize(7).Bold().FontColor("#FFFFFF");
|
|
}
|
|
});
|
|
|
|
if (a.Image != null && a.Image.Length > 0)
|
|
{
|
|
col.Item().PaddingVertical(4).AlignCenter().MaxHeight(MaxAnswerImageHeight).Image(a.Image);
|
|
}
|
|
|
|
ComposeLangBlock(col, "NL", a.Text.GetValueOrDefault("nl"), "#F97316");
|
|
ComposeLangBlock(col, "FR", a.Text.GetValueOrDefault("fr"), "#3B82F6");
|
|
ComposeLangBlock(col, "EN", a.Text.GetValueOrDefault("en"), "#10B981");
|
|
});
|
|
}
|
|
|
|
static void ComposeLangBlock(ColumnDescriptor column, string tag, string? text, string accentColor)
|
|
{
|
|
if (string.IsNullOrWhiteSpace(text)) return;
|
|
|
|
column.Item().PaddingTop(3).BorderLeft(3).BorderColor(accentColor).Background("#F8FAFC").Padding(4).Row(row =>
|
|
{
|
|
row.AutoItem().PaddingRight(5).Text(tag).Bold().FontSize(8).FontColor("#64748B");
|
|
row.RelativeItem().Text(text);
|
|
});
|
|
}
|
|
}
|
|
} |