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
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:
parent
8992c300f1
commit
5db8f8588c
|
|
@ -1,6 +1,6 @@
|
||||||
# Shinkan Jinkendo Version Information
|
# Shinkan Jinkendo Version Information
|
||||||
|
|
||||||
APP_VERSION = "0.8.72"
|
APP_VERSION = "0.8.73"
|
||||||
BUILD_DATE = "2026-05-10"
|
BUILD_DATE = "2026-05-10"
|
||||||
DB_SCHEMA_VERSION = "20260510047"
|
DB_SCHEMA_VERSION = "20260510047"
|
||||||
|
|
||||||
|
|
@ -30,6 +30,13 @@ MODULE_VERSIONS = {
|
||||||
}
|
}
|
||||||
|
|
||||||
CHANGELOG = [
|
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",
|
"version": "0.8.72",
|
||||||
"date": "2026-05-10",
|
"date": "2026-05-10",
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,59 @@
|
||||||
import { useState, useEffect, useCallback } from 'react'
|
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'
|
import api from '../utils/api'
|
||||||
|
|
||||||
|
function escHtml(str) {
|
||||||
|
return String(str ?? '')
|
||||||
|
.replace(/&/g, '&')
|
||||||
|
.replace(/</g, '<')
|
||||||
|
.replace(/>/g, '>')
|
||||||
|
.replace(/"/g, '"')
|
||||||
|
}
|
||||||
|
|
||||||
|
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 | Exportiert am ${new Date().toLocaleDateString('de-DE')}</div>
|
||||||
|
<script>window.onload = function () { window.print(); };<\/script>
|
||||||
|
</body>
|
||||||
|
</html>`)
|
||||||
|
win.document.close()
|
||||||
|
}
|
||||||
|
|
||||||
const DOC_TYPES = [
|
const DOC_TYPES = [
|
||||||
{ key: 'impressum', label: 'Impressum', defaultTitle: 'Impressum' },
|
{ key: 'impressum', label: 'Impressum', defaultTitle: 'Impressum' },
|
||||||
{ key: 'privacy_policy', label: 'Datenschutz', defaultTitle: 'Datenschutzerklärung' },
|
{ 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 (
|
return (
|
||||||
<div
|
<div
|
||||||
className="card"
|
className="card"
|
||||||
|
|
@ -164,6 +216,14 @@ function DocumentRow({ doc, onPublish, onArchive, onEdit, onCopy, onViewAudit })
|
||||||
>
|
>
|
||||||
<Copy size={13} />
|
<Copy size={13} />
|
||||||
</button>
|
</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
|
<button
|
||||||
className="btn btn-secondary"
|
className="btn btn-secondary"
|
||||||
style={{ padding: '4px 10px', fontSize: '0.78rem' }}
|
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) => {
|
const handleEdit = (doc) => {
|
||||||
setEditDoc(doc)
|
setEditDoc(doc)
|
||||||
setShowForm(true)
|
setShowForm(true)
|
||||||
|
|
@ -510,6 +579,7 @@ export default function AdminLegalDocumentsPage() {
|
||||||
onArchive={handleArchive}
|
onArchive={handleArchive}
|
||||||
onEdit={handleEdit}
|
onEdit={handleEdit}
|
||||||
onCopy={handleCopy}
|
onCopy={handleCopy}
|
||||||
|
onPrint={handlePrint}
|
||||||
onViewAudit={handleViewAudit}
|
onViewAudit={handleViewAudit}
|
||||||
/>
|
/>
|
||||||
))
|
))
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,55 @@ import { useState, useEffect } from 'react'
|
||||||
import { Link } from 'react-router-dom'
|
import { Link } from 'react-router-dom'
|
||||||
import api from '../utils/api'
|
import api from '../utils/api'
|
||||||
|
|
||||||
|
function escHtml(str) {
|
||||||
|
return String(str ?? '')
|
||||||
|
.replace(/&/g, '&')
|
||||||
|
.replace(/</g, '<')
|
||||||
|
.replace(/>/g, '>')
|
||||||
|
.replace(/"/g, '"')
|
||||||
|
}
|
||||||
|
|
||||||
|
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 | 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
|
// document_type values used in the DB / API
|
||||||
const TYPE_MAP = {
|
const TYPE_MAP = {
|
||||||
impressum: 'impressum',
|
impressum: 'impressum',
|
||||||
|
|
@ -210,7 +259,18 @@ function LegalPage({ type }) {
|
||||||
</div>
|
</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) => (
|
{sections.map((section, i) => (
|
||||||
<div key={i} style={{ marginBottom: '1.75rem' }}>
|
<div key={i} style={{ marginBottom: '1.75rem' }}>
|
||||||
|
|
|
||||||
|
|
@ -1,13 +1,13 @@
|
||||||
// Shinkan Jinkendo Frontend Version
|
// 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 BUILD_DATE = "2026-05-10"
|
||||||
|
|
||||||
export const PAGE_VERSIONS = {
|
export const PAGE_VERSIONS = {
|
||||||
LoginPage: "1.0.2",
|
LoginPage: "1.0.2",
|
||||||
LegalPage: "1.1.0",
|
|
||||||
SettingsLegalPage: "1.0.0",
|
SettingsLegalPage: "1.0.0",
|
||||||
AdminLegalDocumentsPage: "1.1.0",
|
AdminLegalDocumentsPage: "1.2.0",
|
||||||
|
LegalPage: "1.2.0",
|
||||||
Dashboard: "1.0.0",
|
Dashboard: "1.0.0",
|
||||||
AccountSettingsPage: "1.0.1",
|
AccountSettingsPage: "1.0.1",
|
||||||
ExercisesPage: "1.5.0", // Fokus +/- Regeln, nur ohne Fokusbereich; Filterprefs
|
ExercisesPage: "1.5.0", // Fokus +/- Regeln, nur ohne Fokusbereich; Filterprefs
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user