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
|
||||
|
||||
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",
|
||||
|
|
|
|||
|
|
@ -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, '&')
|
||||
.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 = [
|
||||
{ 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}
|
||||
/>
|
||||
))
|
||||
|
|
|
|||
|
|
@ -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, '&')
|
||||
.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
|
||||
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' }}>
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user