feat(legal): PDF-Export fuer Rechtstexte (Browser-Print)
Some checks failed
Deploy Development / deploy (push) Successful in 47s
Test Suite / pytest-backend (push) Successful in 58s
Test Suite / lint-backend (push) Successful in 0s
Test Suite / build-frontend (push) Successful in 7s
Test Suite / playwright-tests (push) Failing after 49s

printLegalDocument() oeffnet formatiertes Druckfenster mit Titel,
Versionsnummer, Gueltigkeitsdatum und allen Abschnitten.

AdminLegalDocumentsPage: Drucker-Button laedt Volldokument und druckt.
LegalPage: PDF/Drucken-Button neben h1 wenn veroeffentlichtes Dokument geladen.

version: 0.8.73

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Lars 2026-05-10 16:47:52 +02:00
parent 8992c300f1
commit 5db8f8588c
4 changed files with 144 additions and 7 deletions

View File

@ -1,6 +1,6 @@
# Shinkan Jinkendo Version Information
APP_VERSION = "0.8.72"
APP_VERSION = "0.8.73"
BUILD_DATE = "2026-05-10"
DB_SCHEMA_VERSION = "20260510047"
@ -30,6 +30,13 @@ MODULE_VERSIONS = {
}
CHANGELOG = [
{
"version": "0.8.73",
"date": "2026-05-10",
"changes": [
"Rechtstexte: PDF-Export via Browser-Print (kein neues Paket); Drucker-Button in AdminLegalDocumentsPage (laedt Volldokument) und auf LegalPage (nur bei veroeffentlichtem Inhalt); Dokument enthaelt Versionsnummer und Gueltigkeitsdatum",
],
},
{
"version": "0.8.72",
"date": "2026-05-10",

View File

@ -1,7 +1,59 @@
import { useState, useEffect, useCallback } from 'react'
import { FileText, Plus, Edit2, Archive, CheckCircle, Clock, Copy } from 'lucide-react'
import { FileText, Plus, Edit2, Archive, CheckCircle, Clock, Copy, Printer } from 'lucide-react'
import api from '../utils/api'
function escHtml(str) {
return String(str ?? '')
.replace(/&/g, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;')
}
function printLegalDocument(doc) {
const STATUS_DE = { published: 'Veröffentlicht', draft: 'Entwurf', archived: 'Archiviert' }
const dateStr = doc.published_at
? new Date(doc.published_at).toLocaleDateString('de-DE')
: new Date(doc.updated_at || doc.created_at).toLocaleDateString('de-DE')
const metaLine = doc.status === 'published'
? `Version ${doc.version} | Gültig seit ${dateStr}`
: `${STATUS_DE[doc.status] || doc.status} | Version ${doc.version} | Stand ${dateStr}`
const sectionsHtml = (doc.content_sections || []).map(s => `
<h2>${escHtml(s.heading)}</h2>
<p>${escHtml(s.content).replace(/\n/g, '<br>')}</p>
`).join('')
const win = window.open('', '_blank')
win.document.write(`<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="UTF-8">
<title>${escHtml(doc.title)}</title>
<style>
body { font-family: Georgia, serif; max-width: 760px; margin: 40px auto; color: #111; line-height: 1.65; font-size: 14px; }
h1 { font-size: 1.7rem; margin: 0 0 0.25rem; }
h2 { font-size: 1rem; font-family: Arial, sans-serif; margin: 1.6rem 0 0.3rem; }
p { margin: 0; white-space: pre-wrap; }
.meta { color: #555; font-size: 0.88rem; padding-bottom: 1rem; border-bottom: 2px solid #111; margin-bottom: 1.5rem; }
.footer { margin-top: 3rem; padding-top: 0.75rem; border-top: 1px solid #ccc; font-size: 0.78rem; color: #777; text-align: center; }
@media print {
body { margin: 0; }
@page { margin: 20mm 22mm; }
}
</style>
</head>
<body>
<h1>${escHtml(doc.title)}</h1>
<div class="meta">${escHtml(metaLine)}</div>
${sectionsHtml}
<div class="footer">Shinkan Jinkendo &nbsp;|&nbsp; Exportiert am ${new Date().toLocaleDateString('de-DE')}</div>
<script>window.onload = function () { window.print(); };<\/script>
</body>
</html>`)
win.document.close()
}
const DOC_TYPES = [
{ key: 'impressum', label: 'Impressum', defaultTitle: 'Impressum' },
{ key: 'privacy_policy', label: 'Datenschutz', defaultTitle: 'Datenschutzerklärung' },
@ -95,7 +147,7 @@ function DocTypeTab({ docType, active, onClick }) {
)
}
function DocumentRow({ doc, onPublish, onArchive, onEdit, onCopy, onViewAudit }) {
function DocumentRow({ doc, onPublish, onArchive, onEdit, onCopy, onPrint, onViewAudit }) {
return (
<div
className="card"
@ -164,6 +216,14 @@ function DocumentRow({ doc, onPublish, onArchive, onEdit, onCopy, onViewAudit })
>
<Copy size={13} />
</button>
<button
className="btn btn-secondary"
style={{ padding: '4px 10px', fontSize: '0.78rem' }}
onClick={() => onPrint(doc)}
title="Als PDF drucken / exportieren"
>
<Printer size={13} />
</button>
<button
className="btn btn-secondary"
style={{ padding: '4px 10px', fontSize: '0.78rem' }}
@ -389,6 +449,15 @@ export default function AdminLegalDocumentsPage() {
}
}
const handlePrint = async (doc) => {
try {
const full = await api.getLegalDocument(doc.id)
printLegalDocument(full)
} catch (e) {
alert('Fehler beim Laden des Dokuments: ' + e.message)
}
}
const handleEdit = (doc) => {
setEditDoc(doc)
setShowForm(true)
@ -510,6 +579,7 @@ export default function AdminLegalDocumentsPage() {
onArchive={handleArchive}
onEdit={handleEdit}
onCopy={handleCopy}
onPrint={handlePrint}
onViewAudit={handleViewAudit}
/>
))

View File

@ -2,6 +2,55 @@ import { useState, useEffect } from 'react'
import { Link } from 'react-router-dom'
import api from '../utils/api'
function escHtml(str) {
return String(str ?? '')
.replace(/&/g, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;')
}
function printPublishedDocument(doc) {
const dateStr = doc.published_at
? new Date(doc.published_at).toLocaleDateString('de-DE')
: new Date(doc.updated_at).toLocaleDateString('de-DE')
const metaLine = `Version ${doc.version} | Gültig seit ${dateStr}`
const sectionsHtml = (doc.content_sections || []).map(s => `
<h2>${escHtml(s.heading)}</h2>
<p>${escHtml(s.content).replace(/\n/g, '<br>')}</p>
`).join('')
const win = window.open('', '_blank')
win.document.write(`<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="UTF-8">
<title>${escHtml(doc.title)}</title>
<style>
body { font-family: Georgia, serif; max-width: 760px; margin: 40px auto; color: #111; line-height: 1.65; font-size: 14px; }
h1 { font-size: 1.7rem; margin: 0 0 0.25rem; }
h2 { font-size: 1rem; font-family: Arial, sans-serif; margin: 1.6rem 0 0.3rem; }
p { margin: 0; white-space: pre-wrap; }
.meta { color: #555; font-size: 0.88rem; padding-bottom: 1rem; border-bottom: 2px solid #111; margin-bottom: 1.5rem; }
.footer { margin-top: 3rem; padding-top: 0.75rem; border-top: 1px solid #ccc; font-size: 0.78rem; color: #777; text-align: center; }
@media print {
body { margin: 0; }
@page { margin: 20mm 22mm; }
}
</style>
</head>
<body>
<h1>${escHtml(doc.title)}</h1>
<div class="meta">${escHtml(metaLine)}</div>
${sectionsHtml}
<div class="footer">Shinkan Jinkendo &nbsp;|&nbsp; Exportiert am ${new Date().toLocaleDateString('de-DE')}</div>
<script>window.onload = function () { window.print(); };<\/script>
</body>
</html>`)
win.document.close()
}
// document_type values used in the DB / API
const TYPE_MAP = {
impressum: 'impressum',
@ -210,7 +259,18 @@ function LegalPage({ type }) {
</div>
)}
<h1 style={{ marginBottom: '2rem', color: 'var(--text1)' }}>{title}</h1>
<div style={{ display: 'flex', alignItems: 'baseline', gap: '1rem', marginBottom: '2rem', flexWrap: 'wrap' }}>
<h1 style={{ margin: 0, color: 'var(--text1)' }}>{title}</h1>
{apiDoc && (
<button
onClick={() => printPublishedDocument(apiDoc)}
className="btn btn-secondary"
style={{ fontSize: '0.82rem', padding: '4px 12px', flexShrink: 0 }}
>
PDF / Drucken
</button>
)}
</div>
{sections.map((section, i) => (
<div key={i} style={{ marginBottom: '1.75rem' }}>

View File

@ -1,13 +1,13 @@
// Shinkan Jinkendo Frontend Version
export const APP_VERSION = "0.8.72"
export const APP_VERSION = "0.8.73"
export const BUILD_DATE = "2026-05-10"
export const PAGE_VERSIONS = {
LoginPage: "1.0.2",
LegalPage: "1.1.0",
SettingsLegalPage: "1.0.0",
AdminLegalDocumentsPage: "1.1.0",
AdminLegalDocumentsPage: "1.2.0",
LegalPage: "1.2.0",
Dashboard: "1.0.0",
AccountSettingsPage: "1.0.1",
ExercisesPage: "1.5.0", // Fokus +/- Regeln, nur ohne Fokusbereich; Filterprefs