diff --git a/config/current.ver b/config/current.ver index cb174d5..d2d61a7 100644 --- a/config/current.ver +++ b/config/current.ver @@ -1 +1 @@ -1.2.1 \ No newline at end of file +1.2.2 \ No newline at end of file diff --git a/public/assets/js/ui-editor.js b/public/assets/js/ui-editor.js index bb5c67e..340f436 100644 --- a/public/assets/js/ui-editor.js +++ b/public/assets/js/ui-editor.js @@ -1060,6 +1060,16 @@ export function initEditor() { } } + async function confirmTemplateReferences(actionLabel) { + if (!current?.section?.is_template || !current?.id) return true; + const res = await apiAction('templates.references', { method: 'GET', data: { template_id: current.id } }).catch(() => ({})); + const refs = Array.isArray(res?.references) ? res.references : []; + if (!refs.length) return true; + const preview = refs.slice(0, 6).map(r => `${r.name || 'Template'} #${r.id}`).join(', '); + const more = refs.length > 6 ? ` und ${refs.length - 6} weitere` : ''; + return confirm(`Dieses Template wird in ${refs.length} anderen Template(s) verwendet (${preview}${more}). ${actionLabel} trotzdem?`); + } + // Buttons btnSave && (btnSave.onclick = save); btnClear && (btnClear.onclick = clearEditor); @@ -1086,6 +1096,8 @@ export function initEditor() { if (!current?.id) return; if (!currentVersionMeta || Number(currentVersionMeta.is_active) !== 1) return; try { + const okRefs = await confirmTemplateReferences('Deaktivieren'); + if (!okRefs) return; const res = await apiAction('content_versions.deactivate', { method: 'POST', data: { content_id: current.id } }); if (!res?.ok) throw new Error(res?.error || 'Deaktivieren fehlgeschlagen'); await loadVersionsForCurrent({ keepSelection: true, preferredId: currentVersionId }); diff --git a/public/assets/js/ui-list.js b/public/assets/js/ui-list.js index 69108f0..378bf11 100644 --- a/public/assets/js/ui-list.js +++ b/public/assets/js/ui-list.js @@ -88,6 +88,21 @@ async function openTemplateManager(item, section) { let versions = []; let activeId = 0; + const fetchTemplateReferences = async () => { + if (!section?.is_template) return []; + const res = await apiAction('templates.references', { method: 'GET', data: { template_id: item.id } }).catch(() => ({})); + return Array.isArray(res?.references) ? res.references : []; + }; + + const confirmTemplateReferences = async (actionLabel) => { + if (!section?.is_template) return true; + const refs = await fetchTemplateReferences(); + if (!refs.length) return true; + const preview = refs.slice(0, 6).map(r => `${r.name || 'Template'} #${r.id}`).join(', '); + const more = refs.length > 6 ? ` und ${refs.length - 6} weitere` : ''; + return confirm(`Dieses Template wird in ${refs.length} anderen Template(s) verwendet (${preview}${more}). ${actionLabel} trotzdem?`); + }; + const updateDeleteState = () => { const hasActive = !!activeId; if (btnDelete) btnDelete.disabled = hasActive; @@ -176,6 +191,10 @@ async function openTemplateManager(item, section) { const onDeleteItem = async () => { if (activeId) return; + if (section?.is_template) { + const ok = await confirmTemplateReferences('Löschen'); + if (!ok) return; + } if (!delDlg || !delForm) { const res = await apiAction('content.delete', { method: 'POST', data: { id: item.id, section_id: section.id } }); toast(res && res.ok ? 'Gelöscht' : 'Löschen fehlgeschlagen', !!(res && res.ok), { duration: 3000 }); @@ -193,6 +212,10 @@ async function openTemplateManager(item, section) { if (delCancel) delCancel.onclick = () => { delDlg.close(); cleanupDelete(); }; delForm.onsubmit = async (ev) => { ev.preventDefault(); + if (section?.is_template) { + const ok = await confirmTemplateReferences('Löschen'); + if (!ok) return; + } const res = await apiAction('content.delete', { method: 'POST', data: { id: item.id, section_id: section.id } }); delDlg.close(); cleanupDelete(); @@ -223,6 +246,10 @@ async function openTemplateManager(item, section) { return; } if (target.dataset.versionDeactivate !== undefined) { + if (section?.is_template) { + const ok = await confirmTemplateReferences('Deaktivieren'); + if (!ok) return; + } const res = await apiAction('content_versions.deactivate', { method: 'POST', data: { content_id: item.id } }); toast(res && res.ok ? 'Aktive Version deaktiviert' : 'Deaktivieren fehlgeschlagen', !!(res && res.ok)); await loadVersions(); diff --git a/src/ApiKernel.php b/src/ApiKernel.php index c3378c3..fb59c45 100644 --- a/src/ApiKernel.php +++ b/src/ApiKernel.php @@ -2204,6 +2204,104 @@ class ApiKernel ]); } + private function handleTemplateReferences(): void + { + $auth = $this->requireAuth(); + $customerId = (int)($auth['customer_id'] ?? 0); + if ($customerId <= 0) { + $this->fail('Customer context missing', null, 500); + } + + $templateId = (int)$this->val($this->in, ['template_id', 'id', 'template'], 0); + if ($templateId <= 0) { + $this->fail('template_id required', null, 422); + } + + $references = $this->findTemplateReferences($customerId, $templateId); + + $this->respond([ + 'ok' => true, + 'template_id' => $templateId, + 'count' => count($references), + 'references' => $references, + ]); + } + + private function findTemplateReferences(int $customerId, int $templateId): array + { + $out = []; + $matches = function (?string $html) use ($templateId): bool { + if (!$html) return false; + $id = preg_quote((string)$templateId, '/'); + $typePattern = '/data-ref-type\s*=\s*(["\"])template\1/i'; + $idPattern = '/data-ref-id\s*=\s*(["\"])' . $id . '\1/i'; + return (bool)(preg_match($typePattern, $html) && preg_match($idPattern, $html)); + }; + + if ($this->useUnifiedContent()) { + $section = $this->ensureEmailtemplateSection($customerId); + if (!$section) return []; + + $itemsTable = $this->contentItemsTable(); + if (!$this->tableExists($itemsTable)) return []; + $itemCols = $this->resolveContentItemColumns($itemsTable); + $htmlCol = $itemCols['html']; + + $versionsTable = $this->contentVersionsTable(); + $versionCols = ($this->tableExists($versionsTable)) ? $this->resolveContentVersionColumns($versionsTable) : null; + $versionHtmlCol = $versionCols['html'] ?? null; + $versionActiveCol = $versionCols['is_active'] ?? null; + + $select = "i.`id` AS id, i.`name` AS name"; + if ($htmlCol) $select .= ", i.`$htmlCol` AS item_html"; + $join = ''; + if ($versionHtmlCol && $versionActiveCol) { + $select .= ", v.`$versionHtmlCol` AS version_html"; + $join = "LEFT JOIN `$versionsTable` v ON v.`content_id` = i.`id` AND v.`$versionActiveCol` = 1"; + } + $sql = "SELECT $select FROM `$itemsTable` i $join WHERE i.`customer_id` = :cid AND i.`section_id` = :sid AND i.`id` <> :id"; + $stmt = $this->pdo->prepare($sql); + $stmt->execute([':cid' => $customerId, ':sid' => (int)$section['id'], ':id' => $templateId]); + $rows = $stmt->fetchAll() ?: []; + + foreach ($rows as $row) { + $html = (string)($row['version_html'] ?? $row['item_html'] ?? ''); + if ($matches($html)) { + $out[] = [ + 'id' => (int)($row['id'] ?? 0), + 'name' => (string)($row['name'] ?? ''), + ]; + } + } + return $out; + } + + $table = $this->tableMap['templates'] ?? null; + if (!$table || !$this->tableExists($table)) return []; + + [$idCol, $allCols] = $this->resolveIdCol('templates'); + $htmlCol = $this->firstExisting($allCols, ['html', 'body', 'markup', 'content']); + if (!$htmlCol) return []; + $nameCol = $this->conf['columns']['templates']['name'] ?? ($this->firstExisting($allCols, ['name']) ?: $idCol); + + [$tw, $tp] = $this->tenantWhere(['customer_id' => $customerId]); + $sql = "SELECT `$idCol` AS id, `$nameCol` AS name, `$htmlCol` AS html FROM `$table` WHERE `$idCol` <> :id" . $tw; + $stmt = $this->pdo->prepare($sql); + $stmt->bindValue(':id', $templateId, PDO::PARAM_INT); + foreach ($tp as $k => $v) $stmt->bindValue($k, $v); + $stmt->execute(); + $rows = $stmt->fetchAll() ?: []; + foreach ($rows as $row) { + if ($matches((string)($row['html'] ?? ''))) { + $out[] = [ + 'id' => (int)($row['id'] ?? 0), + 'name' => (string)($row['name'] ?? ''), + ]; + } + } + return $out; + } + private function handleExternalRender(): void { $token = trim((string)($this->in['token'] ?? '')); @@ -2536,6 +2634,9 @@ class ApiKernel case 'templates.test_send': $this->handleTemplateTestSend(); break; + case 'templates.references': + $this->handleTemplateReferences(); + break; case 'sections_config.reorder': $this->handleSectionsConfigReorder(); break;