feat(p06): Copyright-Feld und Einwilligungskontext in Rechte-Erklaerung
Some checks failed
Deploy Development / deploy (push) Successful in 40s
Test Suite / pytest-backend (push) Successful in 33s
Test Suite / lint-backend (push) Successful in 0s
Test Suite / build-frontend (push) Successful in 10s
Test Suite / playwright-tests (push) Failing after 1m2s
Some checks failed
Deploy Development / deploy (push) Successful in 40s
Test Suite / pytest-backend (push) Successful in 33s
Test Suite / lint-backend (push) Successful in 0s
Test Suite / build-frontend (push) Successful in 10s
Test Suite / playwright-tests (push) Failing after 1m2s
Migration 049: 4 optionale TEXT-Spalten in media_asset_rights_declarations (person_consent_context, parental_consent_context, music_rights_context, third_party_rights_context) fuer Freitext zum Einwilligungskontext. Backend: - media_rights.py: write_rights_declaration speichert 4 Kontextfelder - media_assets.py: copyright_notice + 4 Kontextfelder in Bulk-Upload, RightsDeclarationBody, MediaAssetPatch, MediaBulkPatchBody - exercises.py: copyright_notice + 4 Kontextfelder in upload_exercise_media, wird in INSERT gespeichert Frontend (alle 3 Formulare): - RightsDeclarationDialog: Copyright-Eingabefeld (immer sichtbar) + Freitext-Textarea bei jeder Ja-Antwort (Personen, Minderjaehrige, Musik, Fremdinhalte) - ExerciseInlineFileMediaModal: gleiche Felder inline im Upload-Tab - ExerciseInlineEmbedModal: gleiche Felder inline - api.js: copyright_notice + 4 Kontextfelder in bulkUploadMediaAssets version: 0.8.77 module: media_rights 1.1.0, media_assets 1.14.0, exercises 2.21.0 Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
42aec79ad1
commit
4bc24b4caf
|
|
@ -290,6 +290,12 @@ def assert_rights_for_exercise_link(cur: Any, asset_id: int, exercise_visibility
|
|||
# Declaration-Log schreiben + Schnellfelder aktualisieren
|
||||
# --------------------------------------------------------------------------
|
||||
|
||||
def _clean_context(val: Any) -> Optional[str]:
|
||||
"""Leere Strings → None, sonst auf 2000 Zeichen kuerzen."""
|
||||
s = (val or "").strip()
|
||||
return s[:2000] if s else None
|
||||
|
||||
|
||||
def write_rights_declaration(
|
||||
cur: Any,
|
||||
asset_id: int,
|
||||
|
|
@ -307,11 +313,11 @@ def write_rights_declaration(
|
|||
media_asset_id, declared_by_profile_id, action_type, target_visibility,
|
||||
declaration_version,
|
||||
rights_holder_confirmed,
|
||||
contains_identifiable_persons, person_consent_confirmed,
|
||||
contains_minors, parental_consent_confirmed,
|
||||
contains_music, music_rights_confirmed,
|
||||
contains_third_party_content, third_party_rights_confirmed
|
||||
) VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s)
|
||||
contains_identifiable_persons, person_consent_confirmed, person_consent_context,
|
||||
contains_minors, parental_consent_confirmed, parental_consent_context,
|
||||
contains_music, music_rights_confirmed, music_rights_context,
|
||||
contains_third_party_content, third_party_rights_confirmed, third_party_rights_context
|
||||
) VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s)
|
||||
RETURNING id""",
|
||||
(
|
||||
asset_id,
|
||||
|
|
@ -322,12 +328,16 @@ def write_rights_declaration(
|
|||
bool(decl.get("rights_holder_confirmed")),
|
||||
decl.get("contains_identifiable_persons"),
|
||||
decl.get("person_consent_confirmed"),
|
||||
_clean_context(decl.get("person_consent_context")),
|
||||
decl.get("contains_minors"),
|
||||
decl.get("parental_consent_confirmed"),
|
||||
_clean_context(decl.get("parental_consent_context")),
|
||||
decl.get("contains_music"),
|
||||
decl.get("music_rights_confirmed"),
|
||||
_clean_context(decl.get("music_rights_context")),
|
||||
decl.get("contains_third_party_content"),
|
||||
decl.get("third_party_rights_confirmed"),
|
||||
_clean_context(decl.get("third_party_rights_context")),
|
||||
),
|
||||
)
|
||||
row = cur.fetchone()
|
||||
|
|
|
|||
18
backend/migrations/049_media_rights_consent_context.sql
Normal file
18
backend/migrations/049_media_rights_consent_context.sql
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
-- Migration 049: P-06 Erweiterung – Einwilligungskontext-Felder und Copyright im Upload-Dialog
|
||||
-- Optionale Freitextfelder fuer den Kontext der Einwilligung (z.B. "Schriftliche Einwilligung
|
||||
-- vom 2026-05-01 liegt vor") sowie copyright_notice direkt beim Upload erfassbar.
|
||||
|
||||
ALTER TABLE media_asset_rights_declarations
|
||||
ADD COLUMN IF NOT EXISTS person_consent_context TEXT,
|
||||
ADD COLUMN IF NOT EXISTS parental_consent_context TEXT,
|
||||
ADD COLUMN IF NOT EXISTS music_rights_context TEXT,
|
||||
ADD COLUMN IF NOT EXISTS third_party_rights_context TEXT;
|
||||
|
||||
COMMENT ON COLUMN media_asset_rights_declarations.person_consent_context IS
|
||||
'Optionaler Freitext: In welchem Zusammenhang liegt die Einwilligung der abgebildeten Personen vor?';
|
||||
COMMENT ON COLUMN media_asset_rights_declarations.parental_consent_context IS
|
||||
'Optionaler Freitext: In welchem Zusammenhang liegt die Einwilligung der Sorgeberechtigten vor?';
|
||||
COMMENT ON COLUMN media_asset_rights_declarations.music_rights_context IS
|
||||
'Optionaler Freitext: Welche Lizenz / GEMA-Regelung liegt fuer die Musik vor?';
|
||||
COMMENT ON COLUMN media_asset_rights_declarations.third_party_rights_context IS
|
||||
'Optionaler Freitext: Auf welcher Grundlage duerfen die enthaltenen Fremdinhalte verwendet werden?';
|
||||
|
|
@ -2527,16 +2527,21 @@ async def upload_exercise_media(
|
|||
description: str = Form(""),
|
||||
context: str = Form("ablauf"),
|
||||
is_primary: bool = Form(False),
|
||||
copyright_notice: Optional[str] = Form(None),
|
||||
# P-06: Rechte-Erklaerung (Pflicht bei Datei-Upload mit neuem media_asset)
|
||||
rights_holder_confirmed: Optional[bool] = Form(None),
|
||||
contains_identifiable_persons: Optional[bool] = Form(None),
|
||||
person_consent_confirmed: Optional[bool] = Form(None),
|
||||
person_consent_context: Optional[str] = Form(None),
|
||||
contains_minors: Optional[bool] = Form(None),
|
||||
parental_consent_confirmed: Optional[bool] = Form(None),
|
||||
parental_consent_context: Optional[str] = Form(None),
|
||||
contains_music: Optional[bool] = Form(None),
|
||||
music_rights_confirmed: Optional[bool] = Form(None),
|
||||
music_rights_context: Optional[str] = Form(None),
|
||||
contains_third_party_content: Optional[bool] = Form(None),
|
||||
third_party_rights_confirmed: Optional[bool] = Form(None),
|
||||
third_party_rights_context: Optional[str] = Form(None),
|
||||
):
|
||||
profile_id = tenant.profile_id
|
||||
if media_type not in ("image", "video", "document", "sketch"):
|
||||
|
|
@ -2775,11 +2780,12 @@ async def upload_exercise_media(
|
|||
if not dest_path.is_file():
|
||||
dest_path.write_bytes(raw)
|
||||
|
||||
clean_cr = (copyright_notice or "").strip() or None
|
||||
cur.execute(
|
||||
"""INSERT INTO media_assets (
|
||||
mime_type, byte_size, sha256, original_filename, visibility, club_id,
|
||||
uploaded_by_profile_id, copyright_notice, storage_backend, storage_key, lifecycle_state
|
||||
) VALUES (%s, %s, %s, %s, %s, %s, %s, NULL, 'local', %s, 'active')
|
||||
) VALUES (%s, %s, %s, %s, %s, %s, %s, %s, 'local', %s, 'active')
|
||||
RETURNING id""",
|
||||
(
|
||||
mime,
|
||||
|
|
@ -2789,6 +2795,7 @@ async def upload_exercise_media(
|
|||
ex_vis,
|
||||
dedupe_club,
|
||||
profile_id,
|
||||
clean_cr,
|
||||
storage_key,
|
||||
),
|
||||
)
|
||||
|
|
@ -2799,12 +2806,16 @@ async def upload_exercise_media(
|
|||
"rights_holder_confirmed": rights_holder_confirmed,
|
||||
"contains_identifiable_persons": contains_identifiable_persons,
|
||||
"person_consent_confirmed": person_consent_confirmed,
|
||||
"person_consent_context": person_consent_context,
|
||||
"contains_minors": contains_minors,
|
||||
"parental_consent_confirmed": parental_consent_confirmed,
|
||||
"parental_consent_context": parental_consent_context,
|
||||
"contains_music": contains_music,
|
||||
"music_rights_confirmed": music_rights_confirmed,
|
||||
"music_rights_context": music_rights_context,
|
||||
"contains_third_party_content": contains_third_party_content,
|
||||
"third_party_rights_confirmed": third_party_rights_confirmed,
|
||||
"third_party_rights_context": third_party_rights_context,
|
||||
}
|
||||
validate_rights_declaration(p06_decl, ex_vis)
|
||||
write_rights_declaration(cur, aid, profile_id, "upload", ex_vis, p06_decl)
|
||||
|
|
|
|||
|
|
@ -82,12 +82,16 @@ class MediaAssetPatch(BaseModel):
|
|||
rights_holder_confirmed: Optional[bool] = None
|
||||
contains_identifiable_persons: Optional[bool] = None
|
||||
person_consent_confirmed: Optional[bool] = None
|
||||
person_consent_context: Optional[str] = Field(None, max_length=2000)
|
||||
contains_minors: Optional[bool] = None
|
||||
parental_consent_confirmed: Optional[bool] = None
|
||||
parental_consent_context: Optional[str] = Field(None, max_length=2000)
|
||||
contains_music: Optional[bool] = None
|
||||
music_rights_confirmed: Optional[bool] = None
|
||||
music_rights_context: Optional[str] = Field(None, max_length=2000)
|
||||
contains_third_party_content: Optional[bool] = None
|
||||
third_party_rights_confirmed: Optional[bool] = None
|
||||
third_party_rights_context: Optional[str] = Field(None, max_length=2000)
|
||||
|
||||
|
||||
class RightsDeclarationBody(BaseModel):
|
||||
|
|
@ -96,12 +100,16 @@ class RightsDeclarationBody(BaseModel):
|
|||
rights_holder_confirmed: bool
|
||||
contains_identifiable_persons: bool
|
||||
person_consent_confirmed: Optional[bool] = None
|
||||
person_consent_context: Optional[str] = Field(None, max_length=2000)
|
||||
contains_minors: bool
|
||||
parental_consent_confirmed: Optional[bool] = None
|
||||
parental_consent_context: Optional[str] = Field(None, max_length=2000)
|
||||
contains_music: bool
|
||||
music_rights_confirmed: Optional[bool] = None
|
||||
music_rights_context: Optional[str] = Field(None, max_length=2000)
|
||||
contains_third_party_content: bool
|
||||
third_party_rights_confirmed: Optional[bool] = None
|
||||
third_party_rights_context: Optional[str] = Field(None, max_length=2000)
|
||||
|
||||
|
||||
class MediaBulkLifecycleBody(BaseModel):
|
||||
|
|
@ -132,6 +140,11 @@ class MediaBulkPatchBody(BaseModel):
|
|||
original_filename: Optional[str] = Field(None, max_length=300)
|
||||
visibility: Optional[str] = Field(None, pattern="^(private|club|official)$")
|
||||
club_id: Optional[int] = None
|
||||
# P-06 Kontext (bei Promotion)
|
||||
person_consent_context: Optional[str] = Field(None, max_length=2000)
|
||||
parental_consent_context: Optional[str] = Field(None, max_length=2000)
|
||||
music_rights_context: Optional[str] = Field(None, max_length=2000)
|
||||
third_party_rights_context: Optional[str] = Field(None, max_length=2000)
|
||||
tags: Optional[list[str]] = None
|
||||
# P-06: Rechterklaerung (gilt fuer alle Assets des Batches bei Promotion)
|
||||
rights_holder_confirmed: Optional[bool] = None
|
||||
|
|
@ -671,6 +684,7 @@ def _ingest_library_media_file(
|
|||
visibility: str,
|
||||
club_id_form: Optional[int],
|
||||
decl: Optional[dict] = None,
|
||||
copyright_notice: Optional[str] = None,
|
||||
) -> dict:
|
||||
"""Neues Archiv-Medium oder aktiver Dedupe-Treffer (sha256 + Sichtbarkeit + Verein). Kein exercise_media.
|
||||
|
||||
|
|
@ -809,11 +823,12 @@ def _ingest_library_media_file(
|
|||
if not dest_path.is_file():
|
||||
dest_path.write_bytes(raw)
|
||||
|
||||
clean_cr = (copyright_notice or "").strip() or None
|
||||
cur.execute(
|
||||
"""INSERT INTO media_assets (
|
||||
mime_type, byte_size, sha256, original_filename, visibility, club_id,
|
||||
uploaded_by_profile_id, copyright_notice, storage_backend, storage_key, lifecycle_state
|
||||
) VALUES (%s, %s, %s, %s, %s, %s, %s, NULL, 'local', %s, 'active')
|
||||
) VALUES (%s, %s, %s, %s, %s, %s, %s, %s, 'local', %s, 'active')
|
||||
RETURNING id""",
|
||||
(
|
||||
mime,
|
||||
|
|
@ -823,6 +838,7 @@ def _ingest_library_media_file(
|
|||
vis,
|
||||
next_cid,
|
||||
profile_id,
|
||||
clean_cr,
|
||||
storage_key,
|
||||
),
|
||||
)
|
||||
|
|
@ -841,16 +857,21 @@ async def bulk_upload_media_assets(
|
|||
files: list[UploadFile] = File(..., description="Mehrere Dateien (jpeg, png, gif, mp4, pdf)"),
|
||||
visibility: str = Form("private"),
|
||||
club_id: Optional[int] = Form(None),
|
||||
copyright_notice: Optional[str] = Form(None),
|
||||
# P-06 Rechterklaerung (gilt fuer alle Dateien des Batches)
|
||||
rights_holder_confirmed: bool = Form(...),
|
||||
contains_identifiable_persons: bool = Form(...),
|
||||
person_consent_confirmed: Optional[bool] = Form(None),
|
||||
person_consent_context: Optional[str] = Form(None),
|
||||
contains_minors: bool = Form(...),
|
||||
parental_consent_confirmed: Optional[bool] = Form(None),
|
||||
parental_consent_context: Optional[str] = Form(None),
|
||||
contains_music: bool = Form(...),
|
||||
music_rights_confirmed: Optional[bool] = Form(None),
|
||||
music_rights_context: Optional[str] = Form(None),
|
||||
contains_third_party_content: bool = Form(...),
|
||||
third_party_rights_confirmed: Optional[bool] = Form(None),
|
||||
third_party_rights_context: Optional[str] = Form(None),
|
||||
):
|
||||
"""Mehrere Dateien ins Archiv; Dedupe wie Übungs-Upload. Pro Datei eigene DB-Transaktion.
|
||||
|
||||
|
|
@ -869,12 +890,16 @@ async def bulk_upload_media_assets(
|
|||
"rights_holder_confirmed": rights_holder_confirmed,
|
||||
"contains_identifiable_persons": contains_identifiable_persons,
|
||||
"person_consent_confirmed": person_consent_confirmed,
|
||||
"person_consent_context": person_consent_context,
|
||||
"contains_minors": contains_minors,
|
||||
"parental_consent_confirmed": parental_consent_confirmed,
|
||||
"parental_consent_context": parental_consent_context,
|
||||
"contains_music": contains_music,
|
||||
"music_rights_confirmed": music_rights_confirmed,
|
||||
"music_rights_context": music_rights_context,
|
||||
"contains_third_party_content": contains_third_party_content,
|
||||
"third_party_rights_confirmed": third_party_rights_confirmed,
|
||||
"third_party_rights_context": third_party_rights_context,
|
||||
}
|
||||
target_vis = (visibility or "private").strip().lower()
|
||||
validate_rights_declaration(decl, target_vis)
|
||||
|
|
@ -901,6 +926,7 @@ async def bulk_upload_media_assets(
|
|||
visibility,
|
||||
club_id,
|
||||
decl=decl,
|
||||
copyright_notice=copyright_notice,
|
||||
)
|
||||
conn.commit()
|
||||
results.append({"filename": fn, "ok": True, **r})
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
# Shinkan Jinkendo Version Information
|
||||
|
||||
APP_VERSION = "0.8.76"
|
||||
APP_VERSION = "0.8.77"
|
||||
BUILD_DATE = "2026-05-11"
|
||||
DB_SCHEMA_VERSION = "20260511048"
|
||||
|
||||
|
|
@ -14,12 +14,12 @@ MODULE_VERSIONS = {
|
|||
"club_join_requests": "1.0.1", # Depends(get_tenant_context)
|
||||
"admin_users": "1.0.0", # GET /api/admin/users
|
||||
"platform_media_storage": "1.0.0", # GET/PUT /api/admin/platform-media-storage (Superadmin-Pfad unter MEDIA_ROOT)
|
||||
"media_rights": "1.0.0", # P-06: zentrales Policy-Modul (validate, coverage, write declaration)
|
||||
"media_assets": "1.13.0", # P-06: Rechte-Erklaerung bei Upload/Promotion; Re-Deklarations-Endpoint; Admin-Legacy-Summary
|
||||
"media_rights": "1.1.0", # P-06+: write_rights_declaration + 4 Kontext-Freitextfelder
|
||||
"media_assets": "1.14.0", # P-06+: copyright_notice im Upload-Dialog; 4 Kontext-Felder in Bulk-Upload + PATCH + Re-Deklaration
|
||||
"groups": "0.1.0",
|
||||
"skills": "0.1.0",
|
||||
"methods": "0.1.0",
|
||||
"exercises": "2.20.0", # P-06: upload_exercise_media + from-asset Rechtspruefung
|
||||
"exercises": "2.21.0", # P-06+: copyright_notice + 4 Kontext-Felder in upload_exercise_media
|
||||
"training_units": "0.2.0",
|
||||
"training_programs": "0.1.0",
|
||||
"planning": "0.8.1", # Vorlagen Leseprüfung library_content_visible_to_profile
|
||||
|
|
@ -31,6 +31,13 @@ MODULE_VERSIONS = {
|
|||
}
|
||||
|
||||
CHANGELOG = [
|
||||
{
|
||||
"version": "0.8.77",
|
||||
"date": "2026-05-11",
|
||||
"changes": [
|
||||
"P-06 Erweiterung: Migration 049 — 4 optionale Freitext-Kontextfelder in media_asset_rights_declarations (person_consent_context, parental_consent_context, music_rights_context, third_party_rights_context); copyright_notice direkt im Upload-Dialog erfassbar; alle drei Dialoge (RightsDeclarationDialog, ExerciseInlineFileMediaModal, ExerciseInlineEmbedModal) und alle Backend-Endpoints aktualisiert.",
|
||||
],
|
||||
},
|
||||
{
|
||||
"version": "0.8.76",
|
||||
"date": "2026-05-11",
|
||||
|
|
|
|||
|
|
@ -11,15 +11,20 @@ import {
|
|||
import { sanitizeInlineMediaCaption } from '../utils/inlineMediaCaption'
|
||||
|
||||
const DECL_INIT = {
|
||||
copyright_notice: '',
|
||||
rights_holder_confirmed: false,
|
||||
contains_identifiable_persons: null,
|
||||
person_consent_confirmed: false,
|
||||
person_consent_context: '',
|
||||
contains_minors: null,
|
||||
parental_consent_confirmed: false,
|
||||
parental_consent_context: '',
|
||||
contains_music: null,
|
||||
music_rights_confirmed: false,
|
||||
music_rights_context: '',
|
||||
contains_third_party_content: null,
|
||||
third_party_rights_confirmed: false,
|
||||
third_party_rights_context: '',
|
||||
}
|
||||
|
||||
function validateDecl(decl) {
|
||||
|
|
@ -178,15 +183,17 @@ export default function ExerciseInlineEmbedModal({
|
|||
Rechte-Erklärung <span style={{ fontWeight: 400, color: 'var(--text3)' }}>(VORLÄUFIG – p06-v1-conservative)</span>
|
||||
</p>
|
||||
|
||||
<div style={{ marginBottom: '10px' }}>
|
||||
<label htmlFor="emb-cr" style={{ display: 'block', fontSize: '0.82rem', fontWeight: 600, marginBottom: '3px' }}>Copyright-Angabe (optional)</label>
|
||||
<input id="emb-cr" type="text" className="form-input" placeholder="z.B. © 2026 Lars Stommer, CC BY-NC"
|
||||
value={decl.copyright_notice} onChange={(e) => setDeclField('copyright_notice', e.target.value)}
|
||||
disabled={busy} style={{ fontSize: '0.85rem' }} />
|
||||
</div>
|
||||
|
||||
<div style={{ display: 'flex', alignItems: 'flex-start', gap: '8px', marginBottom: '10px' }}>
|
||||
<input
|
||||
type="checkbox"
|
||||
id="emb-rhc"
|
||||
checked={decl.rights_holder_confirmed}
|
||||
<input type="checkbox" id="emb-rhc" checked={decl.rights_holder_confirmed}
|
||||
onChange={(e) => setDeclField('rights_holder_confirmed', e.target.checked)}
|
||||
disabled={busy}
|
||||
style={{ marginTop: '3px', flexShrink: 0 }}
|
||||
/>
|
||||
disabled={busy} style={{ marginTop: '3px', flexShrink: 0 }} />
|
||||
<label htmlFor="emb-rhc" style={{ fontSize: '0.85rem' }}>
|
||||
Ich bestätige, dass ich die erforderlichen Rechte an diesem Medium besitze oder zur Veröffentlichung berechtigt bin. *
|
||||
</label>
|
||||
|
|
@ -194,96 +201,72 @@ export default function ExerciseInlineEmbedModal({
|
|||
|
||||
<fieldset style={{ border: 'none', padding: 0, marginBottom: '10px' }}>
|
||||
<legend style={{ fontSize: '0.85rem', fontWeight: 600, marginBottom: '4px' }}>Erkennbare Personen abgebildet? *</legend>
|
||||
<div style={{ display: 'flex', gap: '14px' }}>
|
||||
<label style={{ fontSize: '0.85rem' }}>
|
||||
<input type="radio" name="emb-cip" checked={decl.contains_identifiable_persons === true}
|
||||
onChange={() => setDeclField('contains_identifiable_persons', true)} disabled={busy} /> Ja
|
||||
</label>
|
||||
<label style={{ fontSize: '0.85rem' }}>
|
||||
<input type="radio" name="emb-cip" checked={decl.contains_identifiable_persons === false}
|
||||
onChange={() => setDeclField('contains_identifiable_persons', false)} disabled={busy} /> Nein
|
||||
</label>
|
||||
<div style={{ display: 'flex', gap: '14px', marginBottom: '4px' }}>
|
||||
<label style={{ fontSize: '0.85rem' }}><input type="radio" name="emb-cip" checked={decl.contains_identifiable_persons === true} onChange={() => setDeclField('contains_identifiable_persons', true)} disabled={busy} /> Ja</label>
|
||||
<label style={{ fontSize: '0.85rem' }}><input type="radio" name="emb-cip" checked={decl.contains_identifiable_persons === false} onChange={() => setDeclField('contains_identifiable_persons', false)} disabled={busy} /> Nein</label>
|
||||
</div>
|
||||
{decl.contains_identifiable_persons === true && (
|
||||
<div style={{ display: 'flex', alignItems: 'flex-start', gap: '8px', marginTop: '6px' }}>
|
||||
<input type="checkbox" id="emb-pcc" checked={decl.person_consent_confirmed}
|
||||
onChange={(e) => setDeclField('person_consent_confirmed', e.target.checked)}
|
||||
disabled={busy} style={{ marginTop: '3px', flexShrink: 0 }} />
|
||||
<label htmlFor="emb-pcc" style={{ fontSize: '0.85rem' }}>
|
||||
Einwilligungen aller erkennbaren Personen liegen vor. *
|
||||
</label>
|
||||
<div style={{ paddingLeft: 2 }}>
|
||||
<div style={{ display: 'flex', alignItems: 'flex-start', gap: '8px', marginBottom: '4px' }}>
|
||||
<input type="checkbox" id="emb-pcc" checked={decl.person_consent_confirmed} onChange={(e) => setDeclField('person_consent_confirmed', e.target.checked)} disabled={busy} style={{ marginTop: '3px', flexShrink: 0 }} />
|
||||
<label htmlFor="emb-pcc" style={{ fontSize: '0.85rem' }}>Einwilligungen aller erkennbaren Personen liegen vor. *</label>
|
||||
</div>
|
||||
<label style={{ display: 'block', fontSize: '0.8rem', color: 'var(--text2)', marginBottom: '2px' }}>Einwilligungskontext (optional)</label>
|
||||
<textarea className="form-input" rows={2} placeholder="z.B. Schriftliche Einwilligung vom 2026-05-01" value={decl.person_consent_context} onChange={(e) => setDeclField('person_consent_context', e.target.value)} disabled={busy} style={{ fontSize: '0.82rem', resize: 'vertical' }} />
|
||||
</div>
|
||||
)}
|
||||
</fieldset>
|
||||
|
||||
<fieldset style={{ border: 'none', padding: 0, marginBottom: '10px' }}>
|
||||
<legend style={{ fontSize: '0.85rem', fontWeight: 600, marginBottom: '4px' }}>Minderjährige abgebildet? *</legend>
|
||||
<div style={{ display: 'flex', gap: '14px' }}>
|
||||
<label style={{ fontSize: '0.85rem' }}>
|
||||
<input type="radio" name="emb-cm" checked={decl.contains_minors === true}
|
||||
onChange={() => setDeclField('contains_minors', true)} disabled={busy} /> Ja
|
||||
</label>
|
||||
<label style={{ fontSize: '0.85rem' }}>
|
||||
<input type="radio" name="emb-cm" checked={decl.contains_minors === false}
|
||||
onChange={() => setDeclField('contains_minors', false)} disabled={busy} /> Nein
|
||||
</label>
|
||||
<div style={{ display: 'flex', gap: '14px', marginBottom: '4px' }}>
|
||||
<label style={{ fontSize: '0.85rem' }}><input type="radio" name="emb-cm" checked={decl.contains_minors === true} onChange={() => setDeclField('contains_minors', true)} disabled={busy} /> Ja</label>
|
||||
<label style={{ fontSize: '0.85rem' }}><input type="radio" name="emb-cm" checked={decl.contains_minors === false} onChange={() => setDeclField('contains_minors', false)} disabled={busy} /> Nein</label>
|
||||
</div>
|
||||
{decl.contains_minors === true && (
|
||||
<div style={{ display: 'flex', alignItems: 'flex-start', gap: '8px', marginTop: '6px' }}>
|
||||
<input type="checkbox" id="emb-pcc2" checked={decl.parental_consent_confirmed}
|
||||
onChange={(e) => setDeclField('parental_consent_confirmed', e.target.checked)}
|
||||
disabled={busy} style={{ marginTop: '3px', flexShrink: 0 }} />
|
||||
<label htmlFor="emb-pcc2" style={{ fontSize: '0.85rem' }}>
|
||||
Einwilligungen der Sorgeberechtigten liegen vor. *
|
||||
</label>
|
||||
<div style={{ paddingLeft: 2 }}>
|
||||
<div style={{ display: 'flex', alignItems: 'flex-start', gap: '8px', marginBottom: '4px' }}>
|
||||
<input type="checkbox" id="emb-pcc2" checked={decl.parental_consent_confirmed} onChange={(e) => setDeclField('parental_consent_confirmed', e.target.checked)} disabled={busy} style={{ marginTop: '3px', flexShrink: 0 }} />
|
||||
<label htmlFor="emb-pcc2" style={{ fontSize: '0.85rem' }}>Einwilligungen der Sorgeberechtigten liegen vor. *</label>
|
||||
</div>
|
||||
<label style={{ display: 'block', fontSize: '0.8rem', color: 'var(--text2)', marginBottom: '2px' }}>Einwilligungskontext (optional)</label>
|
||||
<textarea className="form-input" rows={2} placeholder="z.B. Elterliche Einwilligung per E-Mail vom 2026-04-15" value={decl.parental_consent_context} onChange={(e) => setDeclField('parental_consent_context', e.target.value)} disabled={busy} style={{ fontSize: '0.82rem', resize: 'vertical' }} />
|
||||
</div>
|
||||
)}
|
||||
</fieldset>
|
||||
|
||||
<fieldset style={{ border: 'none', padding: 0, marginBottom: '10px' }}>
|
||||
<legend style={{ fontSize: '0.85rem', fontWeight: 600, marginBottom: '4px' }}>Musik enthalten? *</legend>
|
||||
<div style={{ display: 'flex', gap: '14px' }}>
|
||||
<label style={{ fontSize: '0.85rem' }}>
|
||||
<input type="radio" name="emb-cmu" checked={decl.contains_music === true}
|
||||
onChange={() => setDeclField('contains_music', true)} disabled={busy} /> Ja
|
||||
</label>
|
||||
<label style={{ fontSize: '0.85rem' }}>
|
||||
<input type="radio" name="emb-cmu" checked={decl.contains_music === false}
|
||||
onChange={() => setDeclField('contains_music', false)} disabled={busy} /> Nein
|
||||
</label>
|
||||
<div style={{ display: 'flex', gap: '14px', marginBottom: '4px' }}>
|
||||
<label style={{ fontSize: '0.85rem' }}><input type="radio" name="emb-cmu" checked={decl.contains_music === true} onChange={() => setDeclField('contains_music', true)} disabled={busy} /> Ja</label>
|
||||
<label style={{ fontSize: '0.85rem' }}><input type="radio" name="emb-cmu" checked={decl.contains_music === false} onChange={() => setDeclField('contains_music', false)} disabled={busy} /> Nein</label>
|
||||
</div>
|
||||
{decl.contains_music === true && (
|
||||
<div style={{ display: 'flex', alignItems: 'flex-start', gap: '8px', marginTop: '6px' }}>
|
||||
<input type="checkbox" id="emb-mrc" checked={decl.music_rights_confirmed}
|
||||
onChange={(e) => setDeclField('music_rights_confirmed', e.target.checked)}
|
||||
disabled={busy} style={{ marginTop: '3px', flexShrink: 0 }} />
|
||||
<label htmlFor="emb-mrc" style={{ fontSize: '0.85rem' }}>
|
||||
Musikrechte (GEMA / Lizenz) liegen vor. *
|
||||
</label>
|
||||
<div style={{ paddingLeft: 2 }}>
|
||||
<div style={{ display: 'flex', alignItems: 'flex-start', gap: '8px', marginBottom: '4px' }}>
|
||||
<input type="checkbox" id="emb-mrc" checked={decl.music_rights_confirmed} onChange={(e) => setDeclField('music_rights_confirmed', e.target.checked)} disabled={busy} style={{ marginTop: '3px', flexShrink: 0 }} />
|
||||
<label htmlFor="emb-mrc" style={{ fontSize: '0.85rem' }}>Musikrechte (GEMA / Lizenz) liegen vor. *</label>
|
||||
</div>
|
||||
<label style={{ display: 'block', fontSize: '0.8rem', color: 'var(--text2)', marginBottom: '2px' }}>Lizenz / GEMA-Kontext (optional)</label>
|
||||
<textarea className="form-input" rows={2} placeholder="z.B. CC BY 4.0 oder GEMA-Freimeldung Nr. …" value={decl.music_rights_context} onChange={(e) => setDeclField('music_rights_context', e.target.value)} disabled={busy} style={{ fontSize: '0.82rem', resize: 'vertical' }} />
|
||||
</div>
|
||||
)}
|
||||
</fieldset>
|
||||
|
||||
<fieldset style={{ border: 'none', padding: 0, marginBottom: '4px' }}>
|
||||
<legend style={{ fontSize: '0.85rem', fontWeight: 600, marginBottom: '4px' }}>Fremde geschützte Inhalte (Logos, Grafiken, Fotos Dritter)? *</legend>
|
||||
<div style={{ display: 'flex', gap: '14px' }}>
|
||||
<label style={{ fontSize: '0.85rem' }}>
|
||||
<input type="radio" name="emb-ctpc" checked={decl.contains_third_party_content === true}
|
||||
onChange={() => setDeclField('contains_third_party_content', true)} disabled={busy} /> Ja
|
||||
</label>
|
||||
<label style={{ fontSize: '0.85rem' }}>
|
||||
<input type="radio" name="emb-ctpc" checked={decl.contains_third_party_content === false}
|
||||
onChange={() => setDeclField('contains_third_party_content', false)} disabled={busy} /> Nein
|
||||
</label>
|
||||
<div style={{ display: 'flex', gap: '14px', marginBottom: '4px' }}>
|
||||
<label style={{ fontSize: '0.85rem' }}><input type="radio" name="emb-ctpc" checked={decl.contains_third_party_content === true} onChange={() => setDeclField('contains_third_party_content', true)} disabled={busy} /> Ja</label>
|
||||
<label style={{ fontSize: '0.85rem' }}><input type="radio" name="emb-ctpc" checked={decl.contains_third_party_content === false} onChange={() => setDeclField('contains_third_party_content', false)} disabled={busy} /> Nein</label>
|
||||
</div>
|
||||
{decl.contains_third_party_content === true && (
|
||||
<div style={{ display: 'flex', alignItems: 'flex-start', gap: '8px', marginTop: '6px' }}>
|
||||
<input type="checkbox" id="emb-tprc" checked={decl.third_party_rights_confirmed}
|
||||
onChange={(e) => setDeclField('third_party_rights_confirmed', e.target.checked)}
|
||||
disabled={busy} style={{ marginTop: '3px', flexShrink: 0 }} />
|
||||
<label htmlFor="emb-tprc" style={{ fontSize: '0.85rem' }}>
|
||||
Rechte an allen enthaltenen Fremdmaterialien liegen vor. *
|
||||
</label>
|
||||
<div style={{ paddingLeft: 2 }}>
|
||||
<div style={{ display: 'flex', alignItems: 'flex-start', gap: '8px', marginBottom: '4px' }}>
|
||||
<input type="checkbox" id="emb-tprc" checked={decl.third_party_rights_confirmed} onChange={(e) => setDeclField('third_party_rights_confirmed', e.target.checked)} disabled={busy} style={{ marginTop: '3px', flexShrink: 0 }} />
|
||||
<label htmlFor="emb-tprc" style={{ fontSize: '0.85rem' }}>Rechte an allen enthaltenen Fremdmaterialien liegen vor. *</label>
|
||||
</div>
|
||||
<label style={{ display: 'block', fontSize: '0.8rem', color: 'var(--text2)', marginBottom: '2px' }}>Rechtsgrundlage (optional)</label>
|
||||
<textarea className="form-input" rows={2} placeholder="z.B. Verbandslogo mit Genehmigung vom 2026-03-10" value={decl.third_party_rights_context} onChange={(e) => setDeclField('third_party_rights_context', e.target.value)} disabled={busy} style={{ fontSize: '0.82rem', resize: 'vertical' }} />
|
||||
</div>
|
||||
)}
|
||||
</fieldset>
|
||||
|
|
|
|||
|
|
@ -61,15 +61,20 @@ function inferExerciseMediaType(file) {
|
|||
}
|
||||
|
||||
const DECL_INIT = {
|
||||
copyright_notice: '',
|
||||
rights_holder_confirmed: false,
|
||||
contains_identifiable_persons: null,
|
||||
person_consent_confirmed: false,
|
||||
person_consent_context: '',
|
||||
contains_minors: null,
|
||||
parental_consent_confirmed: false,
|
||||
parental_consent_context: '',
|
||||
contains_music: null,
|
||||
music_rights_confirmed: false,
|
||||
music_rights_context: '',
|
||||
contains_third_party_content: null,
|
||||
third_party_rights_confirmed: false,
|
||||
third_party_rights_context: '',
|
||||
}
|
||||
|
||||
function validateDecl(decl) {
|
||||
|
|
@ -414,15 +419,19 @@ export default function ExerciseInlineFileMediaModal({
|
|||
Rechte-Erklärung <span style={{ fontWeight: 400, color: 'var(--text3)' }}>(VORLÄUFIG – p06-v1-conservative)</span>
|
||||
</p>
|
||||
|
||||
<div style={{ marginBottom: '10px' }}>
|
||||
<label htmlFor="up-cr" style={{ display: 'block', fontSize: '0.82rem', fontWeight: 600, marginBottom: '3px' }}>Copyright-Angabe (optional)</label>
|
||||
<input id="up-cr" type="text" className="form-input"
|
||||
placeholder="z.B. © 2026 Lars Stommer, CC BY-NC"
|
||||
value={decl.copyright_notice}
|
||||
onChange={(e) => setDeclField('copyright_notice', e.target.value)}
|
||||
disabled={busy} style={{ fontSize: '0.85rem' }} />
|
||||
</div>
|
||||
|
||||
<div style={{ display: 'flex', alignItems: 'flex-start', gap: '8px', marginBottom: '10px' }}>
|
||||
<input
|
||||
type="checkbox"
|
||||
id="up-rhc"
|
||||
checked={decl.rights_holder_confirmed}
|
||||
<input type="checkbox" id="up-rhc" checked={decl.rights_holder_confirmed}
|
||||
onChange={(e) => setDeclField('rights_holder_confirmed', e.target.checked)}
|
||||
disabled={busy}
|
||||
style={{ marginTop: '3px', flexShrink: 0 }}
|
||||
/>
|
||||
disabled={busy} style={{ marginTop: '3px', flexShrink: 0 }} />
|
||||
<label htmlFor="up-rhc" style={{ fontSize: '0.85rem' }}>
|
||||
Ich bestätige, dass ich die erforderlichen Rechte an diesem Medium besitze oder zur Veröffentlichung berechtigt bin. *
|
||||
</label>
|
||||
|
|
@ -430,96 +439,72 @@ export default function ExerciseInlineFileMediaModal({
|
|||
|
||||
<fieldset style={{ border: 'none', padding: 0, marginBottom: '10px' }}>
|
||||
<legend style={{ fontSize: '0.85rem', fontWeight: 600, marginBottom: '4px' }}>Erkennbare Personen abgebildet? *</legend>
|
||||
<div style={{ display: 'flex', gap: '14px' }}>
|
||||
<label style={{ fontSize: '0.85rem' }}>
|
||||
<input type="radio" name="up-cip" checked={decl.contains_identifiable_persons === true}
|
||||
onChange={() => setDeclField('contains_identifiable_persons', true)} disabled={busy} /> Ja
|
||||
</label>
|
||||
<label style={{ fontSize: '0.85rem' }}>
|
||||
<input type="radio" name="up-cip" checked={decl.contains_identifiable_persons === false}
|
||||
onChange={() => setDeclField('contains_identifiable_persons', false)} disabled={busy} /> Nein
|
||||
</label>
|
||||
<div style={{ display: 'flex', gap: '14px', marginBottom: '4px' }}>
|
||||
<label style={{ fontSize: '0.85rem' }}><input type="radio" name="up-cip" checked={decl.contains_identifiable_persons === true} onChange={() => setDeclField('contains_identifiable_persons', true)} disabled={busy} /> Ja</label>
|
||||
<label style={{ fontSize: '0.85rem' }}><input type="radio" name="up-cip" checked={decl.contains_identifiable_persons === false} onChange={() => setDeclField('contains_identifiable_persons', false)} disabled={busy} /> Nein</label>
|
||||
</div>
|
||||
{decl.contains_identifiable_persons === true && (
|
||||
<div style={{ display: 'flex', alignItems: 'flex-start', gap: '8px', marginTop: '6px' }}>
|
||||
<input type="checkbox" id="up-pcc" checked={decl.person_consent_confirmed}
|
||||
onChange={(e) => setDeclField('person_consent_confirmed', e.target.checked)}
|
||||
disabled={busy} style={{ marginTop: '3px', flexShrink: 0 }} />
|
||||
<label htmlFor="up-pcc" style={{ fontSize: '0.85rem' }}>
|
||||
Einwilligungen aller erkennbaren Personen liegen vor. *
|
||||
</label>
|
||||
<div style={{ paddingLeft: 2 }}>
|
||||
<div style={{ display: 'flex', alignItems: 'flex-start', gap: '8px', marginBottom: '4px' }}>
|
||||
<input type="checkbox" id="up-pcc" checked={decl.person_consent_confirmed} onChange={(e) => setDeclField('person_consent_confirmed', e.target.checked)} disabled={busy} style={{ marginTop: '3px', flexShrink: 0 }} />
|
||||
<label htmlFor="up-pcc" style={{ fontSize: '0.85rem' }}>Einwilligungen aller erkennbaren Personen liegen vor. *</label>
|
||||
</div>
|
||||
<label style={{ display: 'block', fontSize: '0.8rem', color: 'var(--text2)', marginBottom: '2px' }}>Einwilligungskontext (optional)</label>
|
||||
<textarea className="form-input" rows={2} placeholder="z.B. Schriftliche Einwilligung vom 2026-05-01" value={decl.person_consent_context} onChange={(e) => setDeclField('person_consent_context', e.target.value)} disabled={busy} style={{ fontSize: '0.82rem', resize: 'vertical' }} />
|
||||
</div>
|
||||
)}
|
||||
</fieldset>
|
||||
|
||||
<fieldset style={{ border: 'none', padding: 0, marginBottom: '10px' }}>
|
||||
<legend style={{ fontSize: '0.85rem', fontWeight: 600, marginBottom: '4px' }}>Minderjährige abgebildet? *</legend>
|
||||
<div style={{ display: 'flex', gap: '14px' }}>
|
||||
<label style={{ fontSize: '0.85rem' }}>
|
||||
<input type="radio" name="up-cm" checked={decl.contains_minors === true}
|
||||
onChange={() => setDeclField('contains_minors', true)} disabled={busy} /> Ja
|
||||
</label>
|
||||
<label style={{ fontSize: '0.85rem' }}>
|
||||
<input type="radio" name="up-cm" checked={decl.contains_minors === false}
|
||||
onChange={() => setDeclField('contains_minors', false)} disabled={busy} /> Nein
|
||||
</label>
|
||||
<div style={{ display: 'flex', gap: '14px', marginBottom: '4px' }}>
|
||||
<label style={{ fontSize: '0.85rem' }}><input type="radio" name="up-cm" checked={decl.contains_minors === true} onChange={() => setDeclField('contains_minors', true)} disabled={busy} /> Ja</label>
|
||||
<label style={{ fontSize: '0.85rem' }}><input type="radio" name="up-cm" checked={decl.contains_minors === false} onChange={() => setDeclField('contains_minors', false)} disabled={busy} /> Nein</label>
|
||||
</div>
|
||||
{decl.contains_minors === true && (
|
||||
<div style={{ display: 'flex', alignItems: 'flex-start', gap: '8px', marginTop: '6px' }}>
|
||||
<input type="checkbox" id="up-pcc2" checked={decl.parental_consent_confirmed}
|
||||
onChange={(e) => setDeclField('parental_consent_confirmed', e.target.checked)}
|
||||
disabled={busy} style={{ marginTop: '3px', flexShrink: 0 }} />
|
||||
<label htmlFor="up-pcc2" style={{ fontSize: '0.85rem' }}>
|
||||
Einwilligungen der Sorgeberechtigten liegen vor. *
|
||||
</label>
|
||||
<div style={{ paddingLeft: 2 }}>
|
||||
<div style={{ display: 'flex', alignItems: 'flex-start', gap: '8px', marginBottom: '4px' }}>
|
||||
<input type="checkbox" id="up-pcc2" checked={decl.parental_consent_confirmed} onChange={(e) => setDeclField('parental_consent_confirmed', e.target.checked)} disabled={busy} style={{ marginTop: '3px', flexShrink: 0 }} />
|
||||
<label htmlFor="up-pcc2" style={{ fontSize: '0.85rem' }}>Einwilligungen der Sorgeberechtigten liegen vor. *</label>
|
||||
</div>
|
||||
<label style={{ display: 'block', fontSize: '0.8rem', color: 'var(--text2)', marginBottom: '2px' }}>Einwilligungskontext (optional)</label>
|
||||
<textarea className="form-input" rows={2} placeholder="z.B. Elterliche Einwilligung per E-Mail vom 2026-04-15" value={decl.parental_consent_context} onChange={(e) => setDeclField('parental_consent_context', e.target.value)} disabled={busy} style={{ fontSize: '0.82rem', resize: 'vertical' }} />
|
||||
</div>
|
||||
)}
|
||||
</fieldset>
|
||||
|
||||
<fieldset style={{ border: 'none', padding: 0, marginBottom: '10px' }}>
|
||||
<legend style={{ fontSize: '0.85rem', fontWeight: 600, marginBottom: '4px' }}>Musik enthalten? *</legend>
|
||||
<div style={{ display: 'flex', gap: '14px' }}>
|
||||
<label style={{ fontSize: '0.85rem' }}>
|
||||
<input type="radio" name="up-cmu" checked={decl.contains_music === true}
|
||||
onChange={() => setDeclField('contains_music', true)} disabled={busy} /> Ja
|
||||
</label>
|
||||
<label style={{ fontSize: '0.85rem' }}>
|
||||
<input type="radio" name="up-cmu" checked={decl.contains_music === false}
|
||||
onChange={() => setDeclField('contains_music', false)} disabled={busy} /> Nein
|
||||
</label>
|
||||
<div style={{ display: 'flex', gap: '14px', marginBottom: '4px' }}>
|
||||
<label style={{ fontSize: '0.85rem' }}><input type="radio" name="up-cmu" checked={decl.contains_music === true} onChange={() => setDeclField('contains_music', true)} disabled={busy} /> Ja</label>
|
||||
<label style={{ fontSize: '0.85rem' }}><input type="radio" name="up-cmu" checked={decl.contains_music === false} onChange={() => setDeclField('contains_music', false)} disabled={busy} /> Nein</label>
|
||||
</div>
|
||||
{decl.contains_music === true && (
|
||||
<div style={{ display: 'flex', alignItems: 'flex-start', gap: '8px', marginTop: '6px' }}>
|
||||
<input type="checkbox" id="up-mrc" checked={decl.music_rights_confirmed}
|
||||
onChange={(e) => setDeclField('music_rights_confirmed', e.target.checked)}
|
||||
disabled={busy} style={{ marginTop: '3px', flexShrink: 0 }} />
|
||||
<label htmlFor="up-mrc" style={{ fontSize: '0.85rem' }}>
|
||||
Musikrechte (GEMA / Lizenz) liegen vor. *
|
||||
</label>
|
||||
<div style={{ paddingLeft: 2 }}>
|
||||
<div style={{ display: 'flex', alignItems: 'flex-start', gap: '8px', marginBottom: '4px' }}>
|
||||
<input type="checkbox" id="up-mrc" checked={decl.music_rights_confirmed} onChange={(e) => setDeclField('music_rights_confirmed', e.target.checked)} disabled={busy} style={{ marginTop: '3px', flexShrink: 0 }} />
|
||||
<label htmlFor="up-mrc" style={{ fontSize: '0.85rem' }}>Musikrechte (GEMA / Lizenz) liegen vor. *</label>
|
||||
</div>
|
||||
<label style={{ display: 'block', fontSize: '0.8rem', color: 'var(--text2)', marginBottom: '2px' }}>Lizenz / GEMA-Kontext (optional)</label>
|
||||
<textarea className="form-input" rows={2} placeholder="z.B. CC BY 4.0 oder GEMA-Freimeldung Nr. …" value={decl.music_rights_context} onChange={(e) => setDeclField('music_rights_context', e.target.value)} disabled={busy} style={{ fontSize: '0.82rem', resize: 'vertical' }} />
|
||||
</div>
|
||||
)}
|
||||
</fieldset>
|
||||
|
||||
<fieldset style={{ border: 'none', padding: 0, marginBottom: '4px' }}>
|
||||
<legend style={{ fontSize: '0.85rem', fontWeight: 600, marginBottom: '4px' }}>Fremde geschützte Inhalte (Logos, Grafiken, Fotos Dritter)? *</legend>
|
||||
<div style={{ display: 'flex', gap: '14px' }}>
|
||||
<label style={{ fontSize: '0.85rem' }}>
|
||||
<input type="radio" name="up-ctpc" checked={decl.contains_third_party_content === true}
|
||||
onChange={() => setDeclField('contains_third_party_content', true)} disabled={busy} /> Ja
|
||||
</label>
|
||||
<label style={{ fontSize: '0.85rem' }}>
|
||||
<input type="radio" name="up-ctpc" checked={decl.contains_third_party_content === false}
|
||||
onChange={() => setDeclField('contains_third_party_content', false)} disabled={busy} /> Nein
|
||||
</label>
|
||||
<div style={{ display: 'flex', gap: '14px', marginBottom: '4px' }}>
|
||||
<label style={{ fontSize: '0.85rem' }}><input type="radio" name="up-ctpc" checked={decl.contains_third_party_content === true} onChange={() => setDeclField('contains_third_party_content', true)} disabled={busy} /> Ja</label>
|
||||
<label style={{ fontSize: '0.85rem' }}><input type="radio" name="up-ctpc" checked={decl.contains_third_party_content === false} onChange={() => setDeclField('contains_third_party_content', false)} disabled={busy} /> Nein</label>
|
||||
</div>
|
||||
{decl.contains_third_party_content === true && (
|
||||
<div style={{ display: 'flex', alignItems: 'flex-start', gap: '8px', marginTop: '6px' }}>
|
||||
<input type="checkbox" id="up-tprc" checked={decl.third_party_rights_confirmed}
|
||||
onChange={(e) => setDeclField('third_party_rights_confirmed', e.target.checked)}
|
||||
disabled={busy} style={{ marginTop: '3px', flexShrink: 0 }} />
|
||||
<label htmlFor="up-tprc" style={{ fontSize: '0.85rem' }}>
|
||||
Rechte an allen enthaltenen Fremdmaterialien liegen vor. *
|
||||
</label>
|
||||
<div style={{ paddingLeft: 2 }}>
|
||||
<div style={{ display: 'flex', alignItems: 'flex-start', gap: '8px', marginBottom: '4px' }}>
|
||||
<input type="checkbox" id="up-tprc" checked={decl.third_party_rights_confirmed} onChange={(e) => setDeclField('third_party_rights_confirmed', e.target.checked)} disabled={busy} style={{ marginTop: '3px', flexShrink: 0 }} />
|
||||
<label htmlFor="up-tprc" style={{ fontSize: '0.85rem' }}>Rechte an allen enthaltenen Fremdmaterialien liegen vor. *</label>
|
||||
</div>
|
||||
<label style={{ display: 'block', fontSize: '0.8rem', color: 'var(--text2)', marginBottom: '2px' }}>Rechtsgrundlage (optional)</label>
|
||||
<textarea className="form-input" rows={2} placeholder="z.B. Verbandslogo mit Genehmigung vom 2026-03-10" value={decl.third_party_rights_context} onChange={(e) => setDeclField('third_party_rights_context', e.target.value)} disabled={busy} style={{ fontSize: '0.82rem', resize: 'vertical' }} />
|
||||
</div>
|
||||
)}
|
||||
</fieldset>
|
||||
|
|
|
|||
|
|
@ -5,19 +5,20 @@
|
|||
import React, { useState } from 'react'
|
||||
|
||||
const INITIAL = {
|
||||
copyright_notice: '',
|
||||
rights_holder_confirmed: false,
|
||||
contains_identifiable_persons: null,
|
||||
person_consent_confirmed: false,
|
||||
person_consent_context: '',
|
||||
contains_minors: null,
|
||||
parental_consent_confirmed: false,
|
||||
parental_consent_context: '',
|
||||
contains_music: null,
|
||||
music_rights_confirmed: false,
|
||||
music_rights_context: '',
|
||||
contains_third_party_content: null,
|
||||
third_party_rights_confirmed: false,
|
||||
}
|
||||
|
||||
function resetDecl() {
|
||||
return { ...INITIAL }
|
||||
third_party_rights_context: '',
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -37,7 +38,7 @@ export default function RightsDeclarationDialog({
|
|||
isPromotion = false,
|
||||
mode = 'upload',
|
||||
}) {
|
||||
const [decl, setDecl] = useState(resetDecl)
|
||||
const [decl, setDecl] = useState({ ...INITIAL })
|
||||
const [error, setError] = useState('')
|
||||
|
||||
if (!open) return null
|
||||
|
|
@ -60,7 +61,7 @@ export default function RightsDeclarationDialog({
|
|||
if (decl.contains_music && !decl.music_rights_confirmed)
|
||||
return 'Bitte bestätigen, dass die erforderlichen Musikrechte vorliegen.'
|
||||
if (decl.contains_third_party_content === null)
|
||||
return 'Bitte angeben, ob fremde geschützte Inhalte (Logos, Grafiken etc.) enthalten sind.'
|
||||
return 'Bitte angeben, ob fremde geschützte Inhalte enthalten sind.'
|
||||
if (decl.contains_third_party_content && !decl.third_party_rights_confirmed)
|
||||
return 'Bitte bestätigen, dass die Rechte an allen enthaltenen Fremdmaterialien vorliegen.'
|
||||
return ''
|
||||
|
|
@ -71,11 +72,11 @@ export default function RightsDeclarationDialog({
|
|||
if (err) { setError(err); return }
|
||||
setError('')
|
||||
onConfirm({ ...decl })
|
||||
setDecl(resetDecl())
|
||||
setDecl({ ...INITIAL })
|
||||
}
|
||||
|
||||
const handleCancel = () => {
|
||||
setDecl(resetDecl())
|
||||
setDecl({ ...INITIAL })
|
||||
setError('')
|
||||
onCancel()
|
||||
}
|
||||
|
|
@ -108,13 +109,30 @@ export default function RightsDeclarationDialog({
|
|||
</div>
|
||||
|
||||
<div style={{ overflowY: 'auto', flex: 1, padding: '14px 16px' }}>
|
||||
<p style={{ fontSize: '0.82rem', color: 'var(--text3)', marginBottom: 16 }}>
|
||||
<p style={{ fontSize: '0.82rem', color: 'var(--text3)', marginBottom: 14 }}>
|
||||
VORLÄUFIG – Texte noch nicht juristisch geprüft (p06-v1-conservative).
|
||||
{isPromotion && (
|
||||
<> Die bestehende Erklärung gilt nicht für die Sichtbarkeit „{visLabel}". Bitte erneut bestätigen.</>
|
||||
)}
|
||||
</p>
|
||||
|
||||
{/* Copyright */}
|
||||
<div style={{ marginBottom: 14 }}>
|
||||
<label htmlFor="rdlg-cr" style={{ display: 'block', fontSize: '0.85rem', fontWeight: 600, marginBottom: 4 }}>
|
||||
Copyright-Angabe (optional)
|
||||
</label>
|
||||
<input
|
||||
id="rdlg-cr"
|
||||
type="text"
|
||||
className="form-input"
|
||||
placeholder="z.B. © 2026 Lars Stommer, CC BY-NC"
|
||||
value={decl.copyright_notice}
|
||||
onChange={(e) => setField('copyright_notice', e.target.value)}
|
||||
style={{ fontSize: '0.9rem' }}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* T1: Rechteinhaber */}
|
||||
<div style={{ display: 'flex', alignItems: 'flex-start', gap: 10, marginBottom: 14 }}>
|
||||
<input
|
||||
type="checkbox"
|
||||
|
|
@ -129,9 +147,10 @@ export default function RightsDeclarationDialog({
|
|||
</label>
|
||||
</div>
|
||||
|
||||
{/* T2 / T3: Personen */}
|
||||
<fieldset style={{ border: 'none', padding: 0, marginBottom: 14 }}>
|
||||
<legend style={{ fontSize: '0.9rem', fontWeight: 600, marginBottom: 6 }}>Erkennbare Personen abgebildet? *</legend>
|
||||
<div style={{ display: 'flex', gap: 16 }}>
|
||||
<div style={{ display: 'flex', gap: 16, marginBottom: 6 }}>
|
||||
<label style={{ fontSize: '0.9rem' }}>
|
||||
<input type="radio" name="rdlg-cip" checked={decl.contains_identifiable_persons === true}
|
||||
onChange={() => setField('contains_identifiable_persons', true)} /> Ja
|
||||
|
|
@ -142,7 +161,8 @@ export default function RightsDeclarationDialog({
|
|||
</label>
|
||||
</div>
|
||||
{decl.contains_identifiable_persons === true && (
|
||||
<div style={{ display: 'flex', alignItems: 'flex-start', gap: 10, marginTop: 8 }}>
|
||||
<div style={{ paddingLeft: 4 }}>
|
||||
<div style={{ display: 'flex', alignItems: 'flex-start', gap: 10, marginBottom: 6 }}>
|
||||
<input type="checkbox" id="rdlg-pcc" checked={decl.person_consent_confirmed}
|
||||
onChange={(e) => setField('person_consent_confirmed', e.target.checked)}
|
||||
style={{ marginTop: 3, flexShrink: 0 }} />
|
||||
|
|
@ -150,12 +170,25 @@ export default function RightsDeclarationDialog({
|
|||
Einwilligungen aller erkennbaren Personen liegen vor. *
|
||||
</label>
|
||||
</div>
|
||||
<label style={{ display: 'block', fontSize: '0.82rem', color: 'var(--text2)', marginBottom: 3 }}>
|
||||
Einwilligungskontext (optional)
|
||||
</label>
|
||||
<textarea
|
||||
className="form-input"
|
||||
rows={2}
|
||||
placeholder="z.B. Schriftliche Einwilligung vom 2026-05-01, im Archiv abgelegt"
|
||||
value={decl.person_consent_context}
|
||||
onChange={(e) => setField('person_consent_context', e.target.value)}
|
||||
style={{ fontSize: '0.85rem', resize: 'vertical' }}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</fieldset>
|
||||
|
||||
{/* T4 / T5: Minderjährige */}
|
||||
<fieldset style={{ border: 'none', padding: 0, marginBottom: 14 }}>
|
||||
<legend style={{ fontSize: '0.9rem', fontWeight: 600, marginBottom: 6 }}>Minderjährige abgebildet? *</legend>
|
||||
<div style={{ display: 'flex', gap: 16 }}>
|
||||
<div style={{ display: 'flex', gap: 16, marginBottom: 6 }}>
|
||||
<label style={{ fontSize: '0.9rem' }}>
|
||||
<input type="radio" name="rdlg-cm" checked={decl.contains_minors === true}
|
||||
onChange={() => setField('contains_minors', true)} /> Ja
|
||||
|
|
@ -166,7 +199,8 @@ export default function RightsDeclarationDialog({
|
|||
</label>
|
||||
</div>
|
||||
{decl.contains_minors === true && (
|
||||
<div style={{ display: 'flex', alignItems: 'flex-start', gap: 10, marginTop: 8 }}>
|
||||
<div style={{ paddingLeft: 4 }}>
|
||||
<div style={{ display: 'flex', alignItems: 'flex-start', gap: 10, marginBottom: 6 }}>
|
||||
<input type="checkbox" id="rdlg-pcc2" checked={decl.parental_consent_confirmed}
|
||||
onChange={(e) => setField('parental_consent_confirmed', e.target.checked)}
|
||||
style={{ marginTop: 3, flexShrink: 0 }} />
|
||||
|
|
@ -174,12 +208,25 @@ export default function RightsDeclarationDialog({
|
|||
Einwilligungen der Sorgeberechtigten liegen vor. *
|
||||
</label>
|
||||
</div>
|
||||
<label style={{ display: 'block', fontSize: '0.82rem', color: 'var(--text2)', marginBottom: 3 }}>
|
||||
Einwilligungskontext (optional)
|
||||
</label>
|
||||
<textarea
|
||||
className="form-input"
|
||||
rows={2}
|
||||
placeholder="z.B. Elterliche Einwilligung per E-Mail vom 2026-04-15 erhalten"
|
||||
value={decl.parental_consent_context}
|
||||
onChange={(e) => setField('parental_consent_context', e.target.value)}
|
||||
style={{ fontSize: '0.85rem', resize: 'vertical' }}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</fieldset>
|
||||
|
||||
{/* T6 / T7: Musik */}
|
||||
<fieldset style={{ border: 'none', padding: 0, marginBottom: 14 }}>
|
||||
<legend style={{ fontSize: '0.9rem', fontWeight: 600, marginBottom: 6 }}>Musik enthalten? *</legend>
|
||||
<div style={{ display: 'flex', gap: 16 }}>
|
||||
<div style={{ display: 'flex', gap: 16, marginBottom: 6 }}>
|
||||
<label style={{ fontSize: '0.9rem' }}>
|
||||
<input type="radio" name="rdlg-cmu" checked={decl.contains_music === true}
|
||||
onChange={() => setField('contains_music', true)} /> Ja
|
||||
|
|
@ -190,7 +237,8 @@ export default function RightsDeclarationDialog({
|
|||
</label>
|
||||
</div>
|
||||
{decl.contains_music === true && (
|
||||
<div style={{ display: 'flex', alignItems: 'flex-start', gap: 10, marginTop: 8 }}>
|
||||
<div style={{ paddingLeft: 4 }}>
|
||||
<div style={{ display: 'flex', alignItems: 'flex-start', gap: 10, marginBottom: 6 }}>
|
||||
<input type="checkbox" id="rdlg-mrc" checked={decl.music_rights_confirmed}
|
||||
onChange={(e) => setField('music_rights_confirmed', e.target.checked)}
|
||||
style={{ marginTop: 3, flexShrink: 0 }} />
|
||||
|
|
@ -198,12 +246,25 @@ export default function RightsDeclarationDialog({
|
|||
Musikrechte (GEMA / Lizenz) liegen vor. *
|
||||
</label>
|
||||
</div>
|
||||
<label style={{ display: 'block', fontSize: '0.82rem', color: 'var(--text2)', marginBottom: 3 }}>
|
||||
Lizenz / GEMA-Kontext (optional)
|
||||
</label>
|
||||
<textarea
|
||||
className="form-input"
|
||||
rows={2}
|
||||
placeholder="z.B. Creative Commons BY 4.0, Lizenz-Nr. oder GEMA-Freimeldung"
|
||||
value={decl.music_rights_context}
|
||||
onChange={(e) => setField('music_rights_context', e.target.value)}
|
||||
style={{ fontSize: '0.85rem', resize: 'vertical' }}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</fieldset>
|
||||
|
||||
{/* T8 / T9: Fremdinhalte */}
|
||||
<fieldset style={{ border: 'none', padding: 0, marginBottom: 4 }}>
|
||||
<legend style={{ fontSize: '0.9rem', fontWeight: 600, marginBottom: 6 }}>Fremde geschützte Inhalte (Logos, Grafiken, Fotos Dritter)? *</legend>
|
||||
<div style={{ display: 'flex', gap: 16 }}>
|
||||
<div style={{ display: 'flex', gap: 16, marginBottom: 6 }}>
|
||||
<label style={{ fontSize: '0.9rem' }}>
|
||||
<input type="radio" name="rdlg-ctpc" checked={decl.contains_third_party_content === true}
|
||||
onChange={() => setField('contains_third_party_content', true)} /> Ja
|
||||
|
|
@ -214,7 +275,8 @@ export default function RightsDeclarationDialog({
|
|||
</label>
|
||||
</div>
|
||||
{decl.contains_third_party_content === true && (
|
||||
<div style={{ display: 'flex', alignItems: 'flex-start', gap: 10, marginTop: 8 }}>
|
||||
<div style={{ paddingLeft: 4 }}>
|
||||
<div style={{ display: 'flex', alignItems: 'flex-start', gap: 10, marginBottom: 6 }}>
|
||||
<input type="checkbox" id="rdlg-tprc" checked={decl.third_party_rights_confirmed}
|
||||
onChange={(e) => setField('third_party_rights_confirmed', e.target.checked)}
|
||||
style={{ marginTop: 3, flexShrink: 0 }} />
|
||||
|
|
@ -222,6 +284,18 @@ export default function RightsDeclarationDialog({
|
|||
Rechte an allen enthaltenen Fremdmaterialien liegen vor. *
|
||||
</label>
|
||||
</div>
|
||||
<label style={{ display: 'block', fontSize: '0.82rem', color: 'var(--text2)', marginBottom: 3 }}>
|
||||
Rechtsgrundlage (optional)
|
||||
</label>
|
||||
<textarea
|
||||
className="form-input"
|
||||
rows={2}
|
||||
placeholder="z.B. Verbandslogo mit Genehmigung vom 2026-03-10, Lizenz-Nr. …"
|
||||
value={decl.third_party_rights_context}
|
||||
onChange={(e) => setField('third_party_rights_context', e.target.value)}
|
||||
style={{ fontSize: '0.85rem', resize: 'vertical' }}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</fieldset>
|
||||
|
||||
|
|
|
|||
|
|
@ -615,20 +615,26 @@ export async function bulkUploadMediaAssets(files, options = {}) {
|
|||
if (options.club_id != null && options.club_id !== '') {
|
||||
formData.append('club_id', String(options.club_id))
|
||||
}
|
||||
// P-06: Rechte-Erklaerung
|
||||
// Copyright + P-06: Rechte-Erklaerung + Kontextfelder
|
||||
if (options.copyright_notice != null && String(options.copyright_notice).trim())
|
||||
formData.append('copyright_notice', String(options.copyright_notice).trim())
|
||||
const p06Fields = [
|
||||
'rights_holder_confirmed',
|
||||
'contains_identifiable_persons',
|
||||
'person_consent_confirmed',
|
||||
'person_consent_context',
|
||||
'contains_minors',
|
||||
'parental_consent_confirmed',
|
||||
'parental_consent_context',
|
||||
'contains_music',
|
||||
'music_rights_confirmed',
|
||||
'music_rights_context',
|
||||
'contains_third_party_content',
|
||||
'third_party_rights_confirmed',
|
||||
'third_party_rights_context',
|
||||
]
|
||||
for (const f of p06Fields) {
|
||||
if (options[f] != null) formData.append(f, String(options[f]))
|
||||
if (options[f] != null && options[f] !== '') formData.append(f, String(options[f]))
|
||||
}
|
||||
const arr = Array.isArray(files) ? files : [files]
|
||||
for (const f of arr) {
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
// Shinkan Jinkendo Frontend Version
|
||||
|
||||
export const APP_VERSION = "0.8.76"
|
||||
export const APP_VERSION = "0.8.77"
|
||||
export const BUILD_DATE = "2026-05-11"
|
||||
|
||||
export const PAGE_VERSIONS = {
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user