From 81b9e8f60108ecaf6add3dcf0a88beda56f94ec6 Mon Sep 17 00:00:00 2001 From: Lars Date: Tue, 12 May 2026 10:52:06 +0200 Subject: [PATCH 01/27] chore: bump version to 0.8.95 and update legal documents features - Updated app version to 0.8.95 with a new build date of 2026-05-12. - Enhanced legal documents functionality to support section numbering and Markdown formatting in the output. - Updated dependencies in package.json to include 'marked' and 'react-markdown'. - Added new CSS styles for legal document presentation. - Refactored PDF generation logic to incorporate new metadata and improved document structure. Co-Authored-By: Claude Sonnet 4.6 --- backend/version.py | 13 +- frontend/package.json | 5 +- frontend/src/app.css | 77 ++++ frontend/src/components/LegalDocumentBody.jsx | 25 ++ .../src/pages/AdminLegalDocumentsPage.jsx | 98 +---- frontend/src/pages/LegalPage.jsx | 87 +--- frontend/src/utils/legalPdfExport.js | 391 ++++++++++++++++++ 7 files changed, 533 insertions(+), 163 deletions(-) create mode 100644 frontend/src/components/LegalDocumentBody.jsx create mode 100644 frontend/src/utils/legalPdfExport.js diff --git a/backend/version.py b/backend/version.py index bee357d..576abb2 100644 --- a/backend/version.py +++ b/backend/version.py @@ -1,11 +1,11 @@ # Shinkan Jinkendo Version Information -APP_VERSION = "0.8.94" -BUILD_DATE = "2026-05-11" +APP_VERSION = "0.8.95" +BUILD_DATE = "2026-05-12" DB_SCHEMA_VERSION = "20260511053" MODULE_VERSIONS = { - "legal_documents": "1.2.0", # jsPDF-Download auf LegalPage (oeffentlich) + Admin; Abschnitts-Sortierung/-Einfuegen + "legal_documents": "1.3.0", # P-01: Ausgabe §-Nummerierung pro Abschnitt; Markdown im Fließtext + PDF; gem. legalPdfExport "auth": "1.2.3", # P-05b: reset-password min_length=8 via Pydantic PasswordResetConfirm "profiles": "1.7.0", # exercise_list_prefs JSONB (Standard Übungsfilter); Patch via ProfileUpdate + Json() "tenant_context": "1.0.5", # Plattform-Admin: effective_club ohne Header aus Profil active_club_id wenn Verein existiert @@ -34,6 +34,13 @@ MODULE_VERSIONS = { } CHANGELOG = [ + { + "version": "0.8.95", + "date": "2026-05-12", + "changes": [ + "P-01 Rechtstexte: Abschnitte in der Ausgabe mit fortlaufender §1, §2, … (nur Darstellung/PDF, nicht in der DB); Fließtext mit Markdown (react-markdown) inkl. PDF-Rendering (fett/kursiv, Listen, Links, Codeblöcke).", + ], + }, { "version": "0.8.94", "date": "2026-05-11", diff --git a/frontend/package.json b/frontend/package.json index eb10a77..9ad335a 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -10,9 +10,12 @@ "dependencies": { "jspdf": "^4.2.1", "lucide-react": "^0.344.0", + "marked": "^18.0.3", "react": "^18.3.1", "react-dom": "^18.3.1", - "react-router-dom": "^6.22.0" + "react-markdown": "^10.1.0", + "react-router-dom": "^6.22.0", + "remark-breaks": "^4.0.0" }, "devDependencies": { "@vitejs/plugin-react": "^4.2.1", diff --git a/frontend/src/app.css b/frontend/src/app.css index 4d1c5f8..1b9c9f1 100644 --- a/frontend/src/app.css +++ b/frontend/src/app.css @@ -291,6 +291,83 @@ ul > li.card + li.card, margin-top: 2px; } +/* Rechtstexte (P-01): Markdown im Fließtext */ +.legal-doc-body { + font-size: 0.95rem; + line-height: 1.55; + color: var(--text1); +} +.legal-doc-body--muted { + color: var(--text3); + font-style: italic; +} +.legal-doc-body p { + margin: 0 0 0.65em; +} +.legal-doc-body p:last-child { + margin-bottom: 0; +} +.legal-doc-body ul, +.legal-doc-body ol { + margin: 0.4em 0 0.65em 1.25rem; + padding: 0; +} +.legal-doc-body li { + margin: 0.2em 0; +} +.legal-doc-body strong { + font-weight: 700; +} +.legal-doc-body em { + font-style: italic; +} +.legal-doc-body code { + font-family: ui-monospace, monospace; + font-size: 0.88em; + background: var(--surface2); + padding: 0.1em 0.35em; + border-radius: 4px; +} +.legal-doc-body pre { + background: var(--surface2); + padding: 10px 12px; + border-radius: 8px; + overflow-x: auto; + margin: 0.65em 0; + font-size: 0.85em; +} +.legal-doc-body pre code { + background: none; + padding: 0; +} +.legal-doc-body blockquote { + margin: 0.5em 0; + padding-left: 12px; + border-left: 3px solid var(--border2); + color: var(--text2); +} +.legal-doc-body a { + color: var(--accent); +} +.legal-doc-body h1, +.legal-doc-body h2, +.legal-doc-body h3, +.legal-doc-body h4 { + font-size: 1em; + font-weight: 700; + margin: 0.75em 0 0.35em; +} +.legal-doc-body h1:first-child, +.legal-doc-body h2:first-child, +.legal-doc-body h3:first-child { + margin-top: 0; +} +.legal-doc-body hr { + border: none; + border-top: 1px solid var(--border); + margin: 0.85em 0; +} + .form-input { width: 100%; min-width: 0; diff --git a/frontend/src/components/LegalDocumentBody.jsx b/frontend/src/components/LegalDocumentBody.jsx new file mode 100644 index 0000000..8838d3d --- /dev/null +++ b/frontend/src/components/LegalDocumentBody.jsx @@ -0,0 +1,25 @@ +import ReactMarkdown from 'react-markdown' +import remarkBreaks from 'remark-breaks' + +/** + * Rechtstext-Absatz aus Markdown (ohne Roht-HTML; Links mit target=_blank). + */ +export default function LegalDocumentBody({ content, muted }) { + if (content == null || content === '') return null + return ( +
+ ( + + {children} + + ), + }} + > + {content} + +
+ ) +} diff --git a/frontend/src/pages/AdminLegalDocumentsPage.jsx b/frontend/src/pages/AdminLegalDocumentsPage.jsx index cb770e4..af6c928 100644 --- a/frontend/src/pages/AdminLegalDocumentsPage.jsx +++ b/frontend/src/pages/AdminLegalDocumentsPage.jsx @@ -1,90 +1,9 @@ import { useState, useEffect, useCallback } from 'react' -import { jsPDF } from 'jspdf' import { FileText, Plus, Edit2, Archive, CheckCircle, Clock, Copy, Download, ChevronUp, ChevronDown } from 'lucide-react' import api from '../utils/api' +import { generateLegalPdf } from '../utils/legalPdfExport' -// ─── PDF generation ────────────────────────────────────────────────────────── - -function generateLegalPdf(doc) { - const pdf = new jsPDF({ format: 'a4', unit: 'mm' }) - const marginL = 22 - const marginR = 22 - const marginTop = 28 - const pageW = 210 - const contentW = pageW - marginL - marginR - const bottomLimit = 277 // A4 297mm - 20mm bottom margin - let y = marginTop - - const checkBreak = (need) => { - if (y + need > bottomLimit) { - pdf.addPage() - y = marginTop - } - } - - // Title - pdf.setFont('helvetica', 'bold') - pdf.setFontSize(20) - pdf.text(doc.title, marginL, y) - y += 10 - - // Meta line - const STATUS_DE = { published: 'Gültig seit', draft: 'Entwurf — Stand', archived: 'Archiviert — Stand' } - 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 = `Version ${doc.version} | ${STATUS_DE[doc.status] || doc.status} ${dateStr}` - - pdf.setFont('helvetica', 'normal') - pdf.setFontSize(10) - pdf.setTextColor(90, 90, 90) - pdf.text(metaLine, marginL, y) - y += 3 - pdf.setDrawColor(0, 0, 0) - pdf.setLineWidth(0.4) - pdf.line(marginL, y, pageW - marginR, y) - y += 8 - pdf.setTextColor(0, 0, 0) - - // Sections - for (const section of (doc.content_sections || [])) { - checkBreak(14) - pdf.setFont('helvetica', 'bold') - pdf.setFontSize(11) - pdf.text(section.heading || '', marginL, y) - y += 6 - - if (section.content) { - pdf.setFont('helvetica', 'normal') - pdf.setFontSize(10) - const lines = pdf.splitTextToSize(section.content, contentW) - for (const line of lines) { - checkBreak(5) - pdf.text(line, marginL, y) - y += 5 - } - } - y += 5 - } - - // Footer on every page - const total = pdf.getNumberOfPages() - for (let i = 1; i <= total; i++) { - pdf.setPage(i) - pdf.setFont('helvetica', 'normal') - pdf.setFontSize(8) - pdf.setTextColor(150, 150, 150) - const fy = 289 - pdf.text( - `Shinkan Jinkendo | Exportiert am ${new Date().toLocaleDateString('de-DE')}`, - marginL, fy - ) - pdf.text(`Seite ${i} von ${total}`, pageW - marginR, fy, { align: 'right' }) - pdf.setTextColor(0, 0, 0) - } - - pdf.save(`${doc.document_type}_v${doc.version}.pdf`) -} +const PDF_STATUS_META = { published: 'Gültig seit', draft: 'Entwurf — Stand', archived: 'Archiviert — Stand' } // ─── Sub-components ────────────────────────────────────────────────────────── @@ -191,7 +110,12 @@ function SectionEditor({ sections, onChange }) { />
- +