- Bild-Skalierung in Kartenansicht gefixt (object-position: top, 300px) - Admin-Slider für Zeichenlimit (4–32.000) als Modul-Setting - Textarea durch RichTextEditor (Quill.js) ersetzt - PDF: HTML-Parsing, Einzelperson-Filter, Autorisierung für alle User
272 lines
13 KiB
C#
272 lines
13 KiB
C#
using Microsoft.AspNetCore.Mvc;
|
|
using Microsoft.AspNetCore.Authorization;
|
|
using Microsoft.AspNetCore.Http;
|
|
using Microsoft.AspNetCore.Hosting;
|
|
using Oqtane.Shared;
|
|
using Oqtane.Enums;
|
|
using Oqtane.Infrastructure;
|
|
using Oqtane.Controllers;
|
|
using SZUAbsolventenverein.Module.HallOfFame.Services;
|
|
using System.Linq;
|
|
using System.Collections.Generic;
|
|
using System.Threading.Tasks;
|
|
using System.Text.RegularExpressions;
|
|
using System.Net;
|
|
using QuestPDF.Fluent;
|
|
using QuestPDF.Helpers;
|
|
using QuestPDF.Infrastructure;
|
|
|
|
namespace SZUAbsolventenverein.Module.HallOfFame.Controllers
|
|
{
|
|
[Route(ControllerRoutes.ApiRoute)]
|
|
public class HallOfFamePdfController : ModuleControllerBase
|
|
{
|
|
private readonly IHallOfFameService _hallOfFameService;
|
|
private readonly IWebHostEnvironment _environment;
|
|
|
|
public HallOfFamePdfController(IHallOfFameService hallOfFameService, ILogManager logger, IHttpContextAccessor accessor, IWebHostEnvironment environment) : base(logger, accessor)
|
|
{
|
|
_hallOfFameService = hallOfFameService;
|
|
_environment = environment;
|
|
}
|
|
|
|
// GET: api/<controller>?moduleid=x&download=true/false&id=y
|
|
[HttpGet]
|
|
[Authorize(Policy = PolicyNames.ViewModule)]
|
|
public async Task<IActionResult> Get(string moduleid, bool download = false, int? id = null)
|
|
{
|
|
int ModuleId;
|
|
if (int.TryParse(moduleid, out ModuleId) && IsAuthorizedEntityId(EntityNames.Module, ModuleId))
|
|
{
|
|
var entries = await _hallOfFameService.GetHallOfFamesAsync(ModuleId);
|
|
var publishedEntries = entries.Where(e => e.Status == "Published").ToList();
|
|
|
|
// If a specific entry ID is provided, filter to just that entry
|
|
if (id.HasValue)
|
|
{
|
|
publishedEntries = publishedEntries.Where(e => e.HallOfFameId == id.Value).ToList();
|
|
if (!publishedEntries.Any())
|
|
{
|
|
return NotFound();
|
|
}
|
|
}
|
|
|
|
var document = Document.Create(container =>
|
|
{
|
|
foreach (var entry in publishedEntries)
|
|
{
|
|
// Bild laden falls vorhanden
|
|
byte[] imageBytes = null;
|
|
if (!string.IsNullOrEmpty(entry.Image))
|
|
{
|
|
try
|
|
{
|
|
var fullImagePath = System.IO.Path.Combine(
|
|
_environment.WebRootPath, entry.Image.TrimStart('/'));
|
|
if (System.IO.File.Exists(fullImagePath))
|
|
imageBytes = System.IO.File.ReadAllBytes(fullImagePath);
|
|
}
|
|
catch { }
|
|
}
|
|
|
|
container.Page(page =>
|
|
{
|
|
page.Size(PageSizes.A4);
|
|
page.Margin(0);
|
|
|
|
page.Content().Layers(layers =>
|
|
{
|
|
// ── Hintergrundbild (Edge-to-Edge) ──
|
|
if (imageBytes != null)
|
|
{
|
|
layers.Layer().Image(imageBytes).FitUnproportionally();
|
|
}
|
|
else
|
|
{
|
|
layers.Layer().Background("#1A1A2E");
|
|
}
|
|
|
|
// ── Inhalt (PrimaryLayer) ──
|
|
layers.PrimaryLayer()
|
|
.Padding(40)
|
|
.Column(column =>
|
|
{
|
|
// ═══ TITELKARTE (oben) ═══
|
|
column.Item()
|
|
.Border(5f).BorderColor("#20000000")
|
|
.CornerRadius(24)
|
|
.Border(3f).BorderColor("#33000000")
|
|
.CornerRadius(22)
|
|
.Border(1f).BorderColor("#44FFFFFF")
|
|
.Background("#CC1A1A2E")
|
|
.CornerRadius(20)
|
|
.PaddingVertical(28)
|
|
.PaddingHorizontal(36)
|
|
.Column(inner =>
|
|
{
|
|
// Name: groß, dominant, Uppercase
|
|
inner.Item()
|
|
.PaddingBottom(6)
|
|
.Text(entry.Name.ToUpper())
|
|
.FontSize(36)
|
|
.ExtraBold()
|
|
.FontColor(Colors.White)
|
|
.LetterSpacing(0.5f);
|
|
|
|
// Trennlinie
|
|
inner.Item()
|
|
.PaddingVertical(8)
|
|
.Height(1.5f)
|
|
.Background("#55FFFFFF");
|
|
|
|
// Jahr: sekundär, elegant
|
|
inner.Item()
|
|
.PaddingTop(4)
|
|
.Text($"Jahrgang {entry.Year}")
|
|
.FontSize(15)
|
|
.FontColor("#CCFFFFFF")
|
|
.LetterSpacing(1.5f);
|
|
});
|
|
|
|
// ═══ BESCHREIBUNGSKARTE (unten) ═══
|
|
var sections = ConvertHtmlToLines(entry.Description ?? "");
|
|
|
|
column.Item().ExtendVertical().AlignBottom()
|
|
.Border(5f).BorderColor("#20000000")
|
|
.CornerRadius(20)
|
|
.Border(3f).BorderColor("#33000000")
|
|
.CornerRadius(18)
|
|
.Border(1f).BorderColor("#33FFFFFF")
|
|
.Background("#CC1A1A2E")
|
|
.CornerRadius(16)
|
|
.PaddingVertical(24)
|
|
.PaddingHorizontal(32)
|
|
.Column(descColumn =>
|
|
{
|
|
if (sections.Count > 0)
|
|
{
|
|
// Überschrift
|
|
descColumn.Item()
|
|
.PaddingBottom(12)
|
|
.Text("Beschreibung")
|
|
.FontSize(14)
|
|
.SemiBold()
|
|
.FontColor("#B0FFFFFF")
|
|
.LetterSpacing(2f);
|
|
|
|
// Trennlinie
|
|
descColumn.Item()
|
|
.PaddingBottom(14)
|
|
.Height(1f)
|
|
.Background("#33FFFFFF");
|
|
|
|
// Text
|
|
foreach (var line in sections)
|
|
{
|
|
descColumn.Item()
|
|
.PaddingBottom(8)
|
|
.Text(line)
|
|
.FontSize(11)
|
|
.FontColor("#E8FFFFFF")
|
|
.LineHeight(1.5f);
|
|
}
|
|
}
|
|
});
|
|
});
|
|
});
|
|
});
|
|
}
|
|
});
|
|
|
|
byte[] pdfBytes = document.GeneratePdf();
|
|
|
|
if (download)
|
|
{
|
|
return File(pdfBytes, "application/pdf", "HallOfFame.pdf");
|
|
}
|
|
// Inline: PDF wird im Browser angezeigt (Vorschau)
|
|
return File(pdfBytes, "application/pdf");
|
|
}
|
|
else
|
|
{
|
|
_logger.Log(LogLevel.Error, this, LogFunction.Security, "Unauthorized HallOfFame PDF Get Attempt {ModuleId}", moduleid);
|
|
return Forbid();
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Converts HTML content (from Quill.js rich text editor) to a list of plain text lines
|
|
/// suitable for rendering in QuestPDF.
|
|
/// Handles: ol/ul lists → numbered/bulleted lines, p → paragraphs, br → newlines,
|
|
/// strips all other tags and decodes HTML entities.
|
|
/// </summary>
|
|
private static List<string> ConvertHtmlToLines(string html)
|
|
{
|
|
if (string.IsNullOrWhiteSpace(html))
|
|
return new List<string>();
|
|
|
|
var lines = new List<string>();
|
|
|
|
// Process ordered lists: <ol>...</ol>
|
|
html = Regex.Replace(html, @"<ol[^>]*>(.*?)</ol>", match =>
|
|
{
|
|
var listContent = match.Groups[1].Value;
|
|
var items = Regex.Matches(listContent, @"<li[^>]*>(.*?)</li>", RegexOptions.Singleline);
|
|
int counter = 1;
|
|
var result = "";
|
|
foreach (Match item in items)
|
|
{
|
|
var text = StripHtmlTags(item.Groups[1].Value).Trim();
|
|
if (!string.IsNullOrEmpty(text))
|
|
result += $"\n{counter}. {text}";
|
|
counter++;
|
|
}
|
|
return result;
|
|
}, RegexOptions.Singleline | RegexOptions.IgnoreCase);
|
|
|
|
// Process unordered lists: <ul>...</ul>
|
|
html = Regex.Replace(html, @"<ul[^>]*>(.*?)</ul>", match =>
|
|
{
|
|
var listContent = match.Groups[1].Value;
|
|
var items = Regex.Matches(listContent, @"<li[^>]*>(.*?)</li>", RegexOptions.Singleline);
|
|
var result = "";
|
|
foreach (Match item in items)
|
|
{
|
|
var text = StripHtmlTags(item.Groups[1].Value).Trim();
|
|
if (!string.IsNullOrEmpty(text))
|
|
result += $"\n• {text}";
|
|
}
|
|
return result;
|
|
}, RegexOptions.Singleline | RegexOptions.IgnoreCase);
|
|
|
|
// Replace <br>, <br/>, <br /> with newline
|
|
html = Regex.Replace(html, @"<br\s*/?>", "\n", RegexOptions.IgnoreCase);
|
|
|
|
// Replace </p>, </div>, </h1>-</h6> with newline
|
|
html = Regex.Replace(html, @"</(?:p|div|h[1-6])>", "\n", RegexOptions.IgnoreCase);
|
|
|
|
// Strip all remaining HTML tags
|
|
html = StripHtmlTags(html);
|
|
|
|
// Decode HTML entities ( & etc.)
|
|
html = WebUtility.HtmlDecode(html);
|
|
|
|
// Split into lines, trim, filter empty
|
|
var rawLines = html.Split('\n')
|
|
.Select(s => s.Trim())
|
|
.Where(s => !string.IsNullOrEmpty(s))
|
|
.ToList();
|
|
|
|
return rawLines;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Strips all HTML tags from a string, leaving only the text content.
|
|
/// </summary>
|
|
private static string StripHtmlTags(string html)
|
|
{
|
|
return Regex.Replace(html, @"<[^>]+>", "");
|
|
}
|
|
}
|
|
}
|