feat(legal): Als-Entwurf-kopieren für Rechtstexte
Some checks failed
Deploy Development / deploy (push) Successful in 35s
Test Suite / pytest-backend (push) Successful in 33s
Test Suite / lint-backend (push) Successful in 0s
Test Suite / build-frontend (push) Successful in 8s
Test Suite / playwright-tests (push) Failing after 51s

POST /api/admin/legal-documents/{id}/copy-as-draft übernimmt Titel +
Inhalt des Quelldokuments und legt einen neuen Entwurf mit
nächster Versionsnummer an. Funktioniert für alle Status (draft/published/archived).

UI: Copy-Button (⎘) in jeder Dokumentzeile; nach Kopie wird die
Liste automatisch aktualisiert und der neue Entwurf ist sichtbar.

version: 0.8.72
module:  legal_documents 1.1.0

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Lars 2026-05-10 12:25:04 +02:00
parent b9adf6da84
commit 8992c300f1
5 changed files with 97 additions and 6 deletions

View File

@ -384,6 +384,70 @@ def archive_legal_document(
return r2d(updated)
@router.post("/api/admin/legal-documents/{doc_id}/copy-as-draft", status_code=201)
def copy_legal_document_as_draft(
doc_id: int,
session: dict = Depends(require_auth),
):
"""
Kopiert ein beliebiges Dokument (egal welcher Status) als neuen Entwurf mit
nächster Versionsnummer. Inhalt und Titel werden übernommen.
"""
_require_superadmin(session)
profile_id = session["profile_id"]
with get_db() as conn:
cur = get_cursor(conn)
cur.execute(
"SELECT id, document_type, title, content_sections FROM legal_documents WHERE id = %s",
(doc_id,),
)
row = cur.fetchone()
if not row:
raise HTTPException(status_code=404, detail="Dokument nicht gefunden")
src = r2d(row)
import json as _json
sections_json = _json.dumps(src["content_sections"], ensure_ascii=False)
with get_db() as conn:
cur = get_cursor(conn)
cur.execute(
"SELECT COALESCE(MAX(version), 0) FROM legal_documents WHERE document_type = %s",
(src["document_type"],),
)
row2 = cur.fetchone()
next_version = list(row2.values())[0] + 1
cur.execute(
"""
INSERT INTO legal_documents
(document_type, version, title, content_sections, status, change_note, created_by_profile_id)
VALUES (%s, %s, %s, %s::jsonb, 'draft', NULL, %s)
RETURNING id, document_type, version, title, content_sections,
status, change_note, created_at, updated_at
""",
(src["document_type"], next_version, src["title"], sections_json, profile_id),
)
new_row = cur.fetchone()
new_id = list(new_row.values())[0]
cur.execute(
"""
INSERT INTO legal_document_audit
(legal_document_id, action, changed_by_profile_id, change_note)
VALUES (%s, 'created', %s, %s)
""",
(new_id, profile_id, f"Kopie von Version {src.get('version', '?')} (ID {doc_id})"),
)
conn.commit()
return r2d(new_row)
@router.get("/api/admin/legal-documents/{doc_id}/audit")
def get_legal_document_audit(doc_id: int, session: dict = Depends(require_auth)):
"""Änderungslog für ein Dokument."""

View File

@ -1,11 +1,11 @@
# Shinkan Jinkendo Version Information
APP_VERSION = "0.8.71"
APP_VERSION = "0.8.72"
BUILD_DATE = "2026-05-10"
DB_SCHEMA_VERSION = "20260510047"
MODULE_VERSIONS = {
"legal_documents": "1.0.0", # P-01c: Admin-konfigurierbare Rechtstexte (legal_documents + legal_document_audit)
"legal_documents": "1.1.0", # Als-Entwurf-kopieren: POST /api/admin/legal-documents/{id}/copy-as-draft
"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
@ -30,6 +30,13 @@ MODULE_VERSIONS = {
}
CHANGELOG = [
{
"version": "0.8.72",
"date": "2026-05-10",
"changes": [
"Rechtstexte: Als-Entwurf-kopieren — POST /api/admin/legal-documents/{id}/copy-as-draft; Inhalt und Titel werden uebernommen, Versionsnummer inkrementiert",
],
},
{
"version": "0.8.71",
"date": "2026-05-10",

View File

@ -1,5 +1,5 @@
import { useState, useEffect, useCallback } from 'react'
import { FileText, Plus, Eye, Edit2, Archive, CheckCircle, Clock, ChevronDown, ChevronUp } from 'lucide-react'
import { FileText, Plus, Edit2, Archive, CheckCircle, Clock, Copy } from 'lucide-react'
import api from '../utils/api'
const DOC_TYPES = [
@ -95,7 +95,7 @@ function DocTypeTab({ docType, active, onClick }) {
)
}
function DocumentRow({ doc, onPublish, onArchive, onEdit, onViewAudit }) {
function DocumentRow({ doc, onPublish, onArchive, onEdit, onCopy, onViewAudit }) {
return (
<div
className="card"
@ -156,6 +156,14 @@ function DocumentRow({ doc, onPublish, onArchive, onEdit, onViewAudit }) {
<Archive size={13} />
</button>
)}
<button
className="btn btn-secondary"
style={{ padding: '4px 10px', fontSize: '0.78rem' }}
onClick={() => onCopy(doc)}
title="Als neuen Entwurf kopieren"
>
<Copy size={13} />
</button>
<button
className="btn btn-secondary"
style={{ padding: '4px 10px', fontSize: '0.78rem' }}
@ -372,6 +380,15 @@ export default function AdminLegalDocumentsPage() {
}
}
const handleCopy = async (doc) => {
try {
await api.copyLegalDocumentAsDraft(doc.id)
load()
} catch (e) {
alert('Fehler: ' + e.message)
}
}
const handleEdit = (doc) => {
setEditDoc(doc)
setShowForm(true)
@ -492,6 +509,7 @@ export default function AdminLegalDocumentsPage() {
onPublish={handlePublish}
onArchive={handleArchive}
onEdit={handleEdit}
onCopy={handleCopy}
onViewAudit={handleViewAudit}
/>
))

View File

@ -1532,6 +1532,8 @@ export const api = {
}),
archiveLegalDocument: (id) =>
request(`/api/admin/legal-documents/${id}/archive`, { method: 'POST' }),
copyLegalDocumentAsDraft: (id) =>
request(`/api/admin/legal-documents/${id}/copy-as-draft`, { method: 'POST' }),
getLegalDocumentAudit: (id) =>
request(`/api/admin/legal-documents/${id}/audit`),
}

View File

@ -1,13 +1,13 @@
// Shinkan Jinkendo Frontend Version
export const APP_VERSION = "0.8.71"
export const APP_VERSION = "0.8.72"
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.0.0",
AdminLegalDocumentsPage: "1.1.0",
Dashboard: "1.0.0",
AccountSettingsPage: "1.0.1",
ExercisesPage: "1.5.0", // Fokus +/- Regeln, nur ohne Fokusbereich; Filterprefs