prepare(" SELECT d.*, dt.title as type_title, dt.slug as default_slug, p.id as project_id, p.name as project_name, p.domain, p.primary_lang, p.active_languages, p.deepl_api_key, p.ai_provider, p.ai_api_key, p.webhook_url, p.webhook_secret, p.brand_color FROM documents d JOIN doc_types dt ON d.doc_type_id = dt.id JOIN projects p ON d.project_id = p.id WHERE d.id = ? "); $stmt->execute([$docId]); $doc = $stmt->fetch(); if (!$doc) { echo htmlspecialchars(t('editor.error.doc_not_found')); exit; } if (!user_can_access_project($db, (int)$doc['project_id'])) { http_response_code(403); echo htmlspecialchars(t('admin.common.access_denied')); exit; } require_csrf(); $stmtAll = $db->prepare("SELECT lang, title, content, updated_at, scheduled_at FROM translations WHERE document_id = ?"); $stmtAll->execute([$docId]); $allTranslations = []; foreach ($stmtAll->fetchAll() as $row) { $allTranslations[$row['lang']] = $row; } $activeLangs = array_filter(array_map('trim', explode(',', $doc['active_languages'] ?? 'de,en'))); $defaultRefLang = $doc['primary_lang'] ?: 'de'; if ($targetLang === $defaultRefLang) { foreach ($activeLangs as $l) { if ($l !== $targetLang && !empty($allTranslations[$l]['content'])) { $defaultRefLang = $l; break; } } } $sourceLang = strtolower($_GET['ref_lang'] ?? $defaultRefLang); $sourceTrans = $allTranslations[$sourceLang] ?? ['title' => $doc['type_title'], 'content' => t('editor.no_text_placeholder')]; $currentSourceHash = md5($sourceTrans['content'] ?? ''); // DeepL AJAX Bidirektionaler Übersetzungs-Endpunkt if (isset($_POST['action']) && $_POST['action'] === 'deepl_translate') { header('Content-Type: application/json'); $env = load_env_file(); $apiKey = !empty($doc['deepl_api_key']) ? $doc['deepl_api_key'] : ($env['DEEPL_API_KEY'] ?? ''); $reqSourceLang = strtolower($_POST['source_lang'] ?? $sourceLang); $reqTargetLang = strtolower($_POST['target_lang'] ?? $targetLang); $contentToTranslate = $_POST['content'] ?? $sourceTrans['content']; $titleToTranslate = $_POST['title'] ?? $sourceTrans['title']; $resContent = translate_with_deepl($contentToTranslate, $reqSourceLang, $reqTargetLang, $apiKey); if (!$resContent['success']) { echo json_encode(['success' => false, 'error' => $resContent['error']]); exit; } $resTitle = translate_with_deepl($titleToTranslate, $reqSourceLang, $reqTargetLang, $apiKey); $translatedTitle = $resTitle['success'] ? $resTitle['text'] : $titleToTranslate; echo json_encode([ 'success' => true, 'title' => $translatedTitle, 'content' => sanitize_legal_html($resContent['text']) ]); exit; } // Einlesemodus (BETA): alten Rechtstext per KI in die Zielstruktur überführen if (isset($_POST['action']) && $_POST['action'] === 'ai_import') { header('Content-Type: application/json'); $env = load_env_file(); $provider = !empty($doc['ai_provider']) ? $doc['ai_provider'] : ($env['AI_PROVIDER'] ?? 'claude'); $apiKey = !empty($doc['ai_api_key']) ? $doc['ai_api_key'] : ($provider === 'openai' ? ($env['OPENAI_API_KEY'] ?? '') : ($env['CLAUDE_API_KEY'] ?? '')); $sourceType = $_POST['source_type'] ?? ''; if ($sourceType === 'url') { $rawResult = fetch_raw_legal_text(trim($_POST['source_url'] ?? ''), 'url'); } elseif ($sourceType === 'file' && !empty($_FILES['import_file']) && ($_FILES['import_file']['error'] ?? UPLOAD_ERR_NO_FILE) === UPLOAD_ERR_OK) { $rawResult = fetch_raw_legal_text($_FILES['import_file']['tmp_name'], 'file'); } else { $rawResult = ['success' => false, 'error' => t('db.ai_import.empty_content')]; } if (!$rawResult['success']) { echo json_encode(['success' => false, 'error' => $rawResult['error']]); exit; } $importResult = import_legal_text_with_ai($rawResult['text'], $doc['default_slug'], $provider, $apiKey); if (!$importResult['success']) { echo json_encode(['success' => false, 'error' => $importResult['error']]); exit; } echo json_encode([ 'success' => true, 'content' => sanitize_legal_html($importResult['content']), 'fields' => $importResult['fields'] ]); exit; } // Einlesemodus (BETA): bestätigte Firmendaten in die Projekteinstellungen übernehmen if (isset($_POST['action']) && $_POST['action'] === 'ai_import_apply_fields') { header('Content-Type: application/json'); $fields = []; foreach (['company_name', 'address', 'email', 'phone', 'representative', 'register_info'] as $f) { $fields[$f] = trim((string)($_POST['fields'][$f] ?? '')); } update_project_company_fields($db, (int)$doc['project_id'], $fields); log_audit((int)$doc['project_id'], $doc['project_name'], t('editor.audit.ai_import_fields_applied')); echo json_encode(['success' => true]); exit; } // Speichern $pendingPublishConfirm = false; $pendingUnfilledTokens = []; if ($_SERVER['REQUEST_METHOD'] === 'POST') { $action = $_POST['action'] ?? ''; if ($action === 'save_translation') { $title = trim($_POST['title'] ?? ''); $slug = trim($_POST['slug'] ?? ''); $content = sanitize_legal_html($_POST['content'] ?? ''); $status = in_array($_POST['status'] ?? '', ['published', 'draft', 'scheduled'], true) ? $_POST['status'] : 'published'; $changeNote = trim($_POST['change_note'] ?? ''); $scheduledAt = !empty($_POST['scheduled_at']) ? str_replace('T', ' ', trim($_POST['scheduled_at'])) . ':00' : null; $confirmUnfilled = ($_POST['confirm_unfilled'] ?? '') === '1'; $pendingPublishConfirm = false; $pendingUnfilledTokens = []; if ($status === 'published' && !$confirmUnfilled) { $stmtProject = $db->prepare("SELECT * FROM projects WHERE id = ?"); $stmtProject->execute([(int)$doc['project_id']]); $projectRow = $stmtProject->fetch(); $pendingUnfilledTokens = $projectRow ? check_unfilled_placeholders($content, $projectRow) : []; $pendingPublishConfirm = !empty($pendingUnfilledTokens); } $sourceHashToSave = $currentSourceHash; $stmtOld = $db->prepare("SELECT content, title, slug FROM translations WHERE document_id = ? AND lang = ?"); $stmtOld->execute([$docId, $targetLang]); $oldRow = $stmtOld->fetch(); if ($pendingPublishConfirm) { // Nicht speichern: Nutzer muss offene Platzhalter zuerst bestätigen. } elseif ($status === 'scheduled' && !empty($scheduledAt)) { if ($oldRow) { $stmt = $db->prepare(" UPDATE translations SET scheduled_at = ?, scheduled_title = ?, scheduled_slug = ?, scheduled_content = ?, scheduled_note = ? WHERE document_id = ? AND lang = ? "); $stmt->execute([$scheduledAt, $title, $slug, $content, $changeNote, $docId, $targetLang]); } else { $stmt = $db->prepare(" INSERT INTO translations (document_id, lang, title, slug, content, previous_content, status, change_note, source_hash, scheduled_at, scheduled_title, scheduled_slug, scheduled_content, scheduled_note, updated_at) VALUES (?, ?, ?, ?, ?, ?, 'draft', ?, ?, ?, ?, ?, ?, ?, CURRENT_TIMESTAMP) "); $stmt->execute([$docId, $targetLang, $title, $slug, $content, $content, $changeNote, $sourceHashToSave, $scheduledAt, $title, $slug, $content, $changeNote]); } $currentSlugForPreview = $oldRow['slug'] ?? $slug; enqueue_webhook($doc, [ 'event_type' => 'legal_text.scheduled', 'document_id' => $docId, 'slug' => $slug, 'lang' => $targetLang, 'title' => $title, 'status' => 'scheduled', 'change_note' => $changeNote, 'scheduled_at' => date('c', strtotime($scheduledAt)), 'effective_date' => date('c', strtotime($scheduledAt)), 'preview_url' => 'https://' . $doc['domain'] . '/' . $targetLang . '/' . $currentSlugForPreview . '/preview', 'preview_api_url' => 'https://' . $doc['domain'] . '/api/' . $targetLang . '/' . $currentSlugForPreview . '/preview' ]); log_audit((int)$doc['project_id'], $doc['project_name'], t('editor.audit.scheduled_for', ['title' => $title, 'lang' => strtoupper($targetLang), 'date' => date('d.m.Y H:i', strtotime($scheduledAt))])); } else { $prevContent = $oldRow ? $oldRow['content'] : $content; $stmt = $db->prepare(" INSERT INTO translations (document_id, lang, title, slug, content, previous_content, status, change_note, source_hash, scheduled_at, scheduled_title, scheduled_slug, scheduled_content, scheduled_note, updated_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, NULL, '', '', '', '', CURRENT_TIMESTAMP) ON CONFLICT(document_id, lang) DO UPDATE SET title=excluded.title, slug=excluded.slug, previous_content=translations.content, content=excluded.content, status=excluded.status, change_note=excluded.change_note, source_hash=excluded.source_hash, scheduled_at=NULL, scheduled_title='', scheduled_slug='', scheduled_content='', scheduled_note='', updated_at=CURRENT_TIMESTAMP "); $stmt->execute([$docId, $targetLang, $title, $slug, $content, $prevContent, $status, $changeNote, $sourceHashToSave]); record_translation_version($db, $docId, $targetLang, $title, $slug, $content, $changeNote, $status); if ($status === 'published') { enqueue_webhook($doc, [ 'event_type' => 'legal_text.updated', 'document_id' => $docId, 'slug' => $slug, 'lang' => $targetLang, 'title' => $title, 'status' => 'published', 'change_note' => $changeNote, 'was_scheduled' => false, 'effective_date' => date('c'), 'updated_at' => date('c') ]); } $statusLabel = $status === 'published' ? t('editor.audit.status_published') : t('editor.audit.status_draft'); log_audit((int)$doc['project_id'], $doc['project_name'], t('editor.audit.saved', ['title' => $title, 'lang' => strtoupper($targetLang), 'status' => $statusLabel])); } if (!$pendingPublishConfirm) { header("Location: /admin?project_id=" . ((int)$doc['project_id']) . "&msg=saved"); exit; } } if ($action === 'restore_version') { $versionId = (int)($_POST['version_id'] ?? 0); $stmtV = $db->prepare("SELECT * FROM translation_versions WHERE id = ? AND document_id = ? AND lang = ?"); $stmtV->execute([$versionId, $docId, $targetLang]); $version = $stmtV->fetch(); if ($version) { $stmtOld = $db->prepare("SELECT content FROM translations WHERE document_id = ? AND lang = ?"); $stmtOld->execute([$docId, $targetLang]); $oldRow = $stmtOld->fetch(); $prevContent = $oldRow ? $oldRow['content'] : $version['content']; $noteRestored = t('editor.restored_note', ['date' => date('d.m.Y H:i', strtotime($version['created_at']))]); $stmt = $db->prepare(" INSERT INTO translations (document_id, lang, title, slug, content, previous_content, status, change_note, source_hash, scheduled_at, scheduled_title, scheduled_slug, scheduled_content, scheduled_note, updated_at) VALUES (?, ?, ?, ?, ?, ?, 'published', ?, ?, NULL, '', '', '', '', CURRENT_TIMESTAMP) ON CONFLICT(document_id, lang) DO UPDATE SET title=excluded.title, slug=excluded.slug, previous_content=translations.content, content=excluded.content, status='published', change_note=excluded.change_note, source_hash=excluded.source_hash, scheduled_at=NULL, scheduled_title='', scheduled_slug='', scheduled_content='', scheduled_note='', updated_at=CURRENT_TIMESTAMP "); $stmt->execute([$docId, $targetLang, $version['title'], $version['slug'], $version['content'], $prevContent, $noteRestored, $currentSourceHash]); record_translation_version($db, $docId, $targetLang, $version['title'], $version['slug'], $version['content'], $noteRestored, 'published'); enqueue_webhook($doc, [ 'event_type' => 'legal_text.updated', 'document_id' => $docId, 'slug' => $version['slug'], 'lang' => $targetLang, 'title' => $version['title'], 'status' => 'published', 'change_note' => $noteRestored, 'was_scheduled' => false, 'effective_date' => date('c'), 'updated_at' => date('c') ]); log_audit((int)$doc['project_id'], $doc['project_name'], t('editor.audit.restored', ['title' => $version['title'], 'lang' => strtoupper($targetLang), 'note' => $noteRestored])); } header("Location: /admin/edit?doc_id=$docId&lang=$targetLang&msg=restored"); exit; } } // Zieltext laden $stmt = $db->prepare("SELECT * FROM translations WHERE document_id = ? AND lang = ?"); $stmt->execute([$docId, $targetLang]); $targetTrans = $stmt->fetch() ?: [ 'title' => $doc['type_title'], 'slug' => $doc['default_slug'], 'content' => '', 'previous_content' => '', 'change_note' => '', 'status' => 'published', 'scheduled_at' => null, 'scheduled_title' => '', 'scheduled_slug' => '', 'scheduled_content' => '', 'scheduled_note' => '' ]; $hasScheduled = !empty($targetTrans['scheduled_at']); $displayContent = $hasScheduled && !empty($targetTrans['scheduled_content']) ? $targetTrans['scheduled_content'] : $targetTrans['content']; $displayTitle = $hasScheduled && !empty($targetTrans['scheduled_title']) ? $targetTrans['scheduled_title'] : $targetTrans['title']; $displaySlug = $hasScheduled && !empty($targetTrans['scheduled_slug']) ? $targetTrans['scheduled_slug'] : $targetTrans['slug']; $displayNote = $hasScheduled && !empty($targetTrans['scheduled_note']) ? $targetTrans['scheduled_note'] : $targetTrans['change_note']; $displayStatus = $hasScheduled ? 'scheduled' : $targetTrans['status']; if ($pendingPublishConfirm) { $displayContent = $content; $displayTitle = $title; $displaySlug = $slug; $displayNote = $changeNote; $displayStatus = 'published'; } $isOutdated = ($targetLang !== $sourceLang && !empty($targetTrans['source_hash']) && $targetTrans['source_hash'] !== $currentSourceHash); $showRef = count($activeLangs) > 1 && ($_GET['showRef'] ?? '0') === '1'; $stmtVersions = $db->prepare("SELECT * FROM translation_versions WHERE document_id = ? AND lang = ? ORDER BY created_at DESC LIMIT 50"); $stmtVersions->execute([$docId, $targetLang]); $versions = $stmtVersions->fetchAll(); $isStandardDocType = find_reference_template_for_slug($doc['default_slug']) !== null; ?> <?= htmlspecialchars(t('editor.page_title', ['doc' => $doc['type_title'], 'lang' => strtoupper($targetLang)])) ?>
/ $doc['type_title']])) ?>
date('d.m.Y \u\m H:i', strtotime($targetTrans['scheduled_at']))]) ?>
implode(', ', $pendingUnfilledTokens)])) ?>
1): ?>

strtoupper(htmlspecialchars($targetLang))]) ?>


strtoupper(htmlspecialchars($targetLang))]) ?>

date('d.m.Y H:i', strtotime($v['created_at']))]) ?>

BETA