mein-sterntours/resources/views/navigation/index.blade.php
Kevin Adametz c39d86f80b Seiten-Sitemap mit Vorschaubild-Verwaltung im CRM ergänzen.
Der Nested-Set-Baum zeigt box_image_url, box_body und Reiseprogramm-Bilder
inklusive Bearbeitung per Modal und Mediathek, damit fehlende Kachelbilder
schneller gefunden und korrigiert werden können.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-18 10:46:22 +00:00

1151 lines
44 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

@extends('layouts.layout-2')
@section('content')
<h4 class="font-weight-bold py-3 mb-1">
<i class="ion ion-md-map"></i> Seiten-Sitemap & Navigation
</h4>
<ul class="nav nav-tabs mb-3" id="tree-view-tabs">
<li class="nav-item">
<a class="nav-link active" href="#" data-view="sitemap">Seiten-Sitemap</a>
</li>
<li class="nav-item">
<a class="nav-link" href="#" data-view="header">Header-Navigation</a>
</li>
</ul>
<div class="alert alert-info" id="view-info-sitemap">
<i class="fa fa-info-circle"></i> Vollständiger Seitenbaum vom Nested-Set (<code>lft</code>/<code>rgt</code>/<code>lvl</code>)
— von der Startseite über Reisekategorien bis zu einzelnen Reisen.
Fehlende Vorschaubilder werden markiert (<code>box_image_url</code>, <code>box_body</code>-JSON oder Reiseprogramm-Bild).
</div>
<div class="alert alert-info" id="view-info-header" style="display:none;">
<i class="fa fa-info-circle"></i> Diese Ansicht zeigt nur die Header-Navigation wie im Frontend
(<code>header.html.twig</code>) — nicht den vollständigen Seitenbaum.
</div>
<!-- Statistiken -->
<div class="row mb-4">
<div class="col-md-2">
<div class="card">
<div class="card-body text-center">
<div class="display-4 text-primary" id="stat-total">-</div>
<small class="text-muted">Gesamt Seiten</small>
</div>
</div>
</div>
<div class="col-md-2">
<div class="card">
<div class="card-body text-center">
<div class="display-4 text-success" id="stat-active">-</div>
<small class="text-muted">Aktive Seiten</small>
</div>
</div>
</div>
<div class="col-md-2">
<div class="card">
<div class="card-body text-center">
<div class="display-4 text-warning" id="stat-programs">-</div>
<small class="text-muted">Reiseprogramme</small>
</div>
</div>
</div>
<div class="col-md-2">
<div class="card">
<div class="card-body text-center">
<div class="display-4 text-info" id="stat-countries">-</div>
<small class="text-muted">Länderseiten</small>
</div>
</div>
</div>
<div class="col-md-2">
<div class="card">
<div class="card-body text-center">
<div class="display-4 text-danger" id="stat-missing-preview">-</div>
<small class="text-muted">Ohne Vorschaubild</small>
</div>
</div>
</div>
</div>
<!-- Toolbar -->
<div class="card mb-3">
<div class="card-body">
<div class="row align-items-center">
<div class="col-md-4">
<div class="input-group">
<input type="text" id="search-input" class="form-control"
placeholder="Suche nach Titel, Slug oder URL...">
<div class="input-group-append">
<button class="btn btn-primary" id="search-btn"><i class="fa fa-search"></i></button>
</div>
</div>
</div>
<div class="col-md-8 text-right">
<div class="btn-group" role="group">
<button class="btn btn-outline-secondary" id="filter-inactive-btn" data-filter="active_only">
<i class="fa fa-eye"></i> Inaktive einblenden
</button>
</div>
<div class="btn-group ml-2" role="group">
<button class="btn btn-warning" id="clear-cache-btn"><i class="fa fa-sync"></i> Cache
leeren</button>
</div>
</div>
</div>
</div>
</div>
<!-- Navigationsbaum -->
<div class="card">
<div class="card-header">
<h5 class="mb-0">
<i class="fa fa-sitemap"></i> Navigationsbaum
<span class="badge badge-primary ml-2" id="node-count">0 Knoten</span>
</h5>
</div>
<div class="card-body">
<div id="loading" class="text-center py-5">
<div class="spinner-border text-primary" role="status">
<span class="sr-only">Lade...</span>
</div>
<p class="mt-3">Lade Navigationsbaum...</p>
</div>
<div id="tree-container" style="display: none;">
<div id="navigation-tree"></div>
</div>
<div id="no-results" style="display: none;" class="text-center py-5 text-muted">
<i class="fa fa-search fa-3x mb-3"></i>
<p>Keine Ergebnisse gefunden.</p>
</div>
</div>
</div>
<div class="modal fade" id="modal-preview-edit" tabindex="-1" role="dialog">
<div class="modal-dialog modal-lg">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">Vorschaubild bearbeiten</h5>
<button type="button" class="close" data-dismiss="modal" aria-label="Schließen"><span aria-hidden="true">×</span></button>
</div>
<div class="modal-body">
<div class="alert alert-light border mb-3">
<p class="mb-2"><strong>Wie die Vorschaubilder zusammenspielen</strong></p>
<ul class="mb-0 pl-3">
<li class="mb-2">
<span class="badge badge-preview-box">box_image_url</span>
<strong>Seitenbild (grüner Rahmen):</strong>
Das Bild, das direkt an dieser Seite hinterlegt ist. Es wird in Kacheln und Übersichten bevorzugt angezeigt.
</li>
<li class="mb-2">
<span class="badge badge-preview-travel">travel_program_image</span>
<strong>Reiseprogramm-Bild (blauer Rahmen):</strong>
Bild aus der Reiseverwaltung auf
<a href="{{ config('app.url_v2') }}" target="_blank" rel="noopener noreferrer">{{ config('app.url_v2') }}</a>.
Dort ist pro Programm ein „Standardbild (Box und Suche)" hinterlegt. Fehlt dieses, wird automatisch das erste Programmbild verwendet.
</li>
<li>
Reine Kategorie- oder Infoseiten ohne verknüpftes Reiseprogramm haben <strong>kein</strong> Reiseprogramm-Bild — dort kann nur ein Seitenbild gesetzt werden.
</li>
</ul>
</div>
<div class="mb-3">
<strong id="preview-edit-page-title">—</strong>
<div class="text-muted small" id="preview-edit-page-url">—</div>
</div>
<div class="mb-3 preview-edit-current-images" id="preview-edit-current-images"></div>
<div class="card border-danger mb-3" id="preview-edit-delete-boxbody-section" style="display:none;">
<div class="card-body">
<h6 class="card-title text-danger mb-2">Legacy-Bild entfernen (box_body)</h6>
<p class="small text-muted mb-3">
Entfernt das ältere, direkt in <code>box_body</code> gespeicherte Bild. Danach gilt automatisch
das normale Seitenbild (<code>box_image_url</code>) oder — falls nicht vorhanden — das Reiseprogramm-Bild.
</p>
<button type="button" class="btn btn-outline-danger btn-sm" id="preview-edit-delete-boxbody-btn">
<i class="fa fa-trash-alt"></i> box_body-Bild entfernen
</button>
</div>
</div>
<div class="card border-danger mb-3" id="preview-edit-delete-section" style="display:none;">
<div class="card-body">
<h6 class="card-title text-danger mb-2">Verknüpftes Seitenbild entfernen</h6>
<p class="small text-muted mb-3">
Entfernt das grüne Seitenbild (<code>box_image_url</code>). Auf der Website wird danach automatisch
das blaue Reiseprogramm-Bild angezeigt — aber nur, wenn für diese Seite ein Reiseprogramm mit Bild existiert.
</p>
<button type="button" class="btn btn-outline-danger btn-sm" id="preview-edit-delete-btn">
<i class="fa fa-trash-alt"></i> Seitenbild entfernen
</button>
</div>
</div>
<div class="card border-success">
<div class="card-body">
<h6 class="card-title text-success mb-2">Neues Seitenbild setzen</h6>
<p class="small text-muted mb-3">
Wählen Sie ein Bild aus der Mediathek. Es wird als <code>box_image_url</code> an dieser Seite gespeichert
und ersetzt ein eventuell vorhandenes Seitenbild.
</p>
<div class="media" id="lfm-preview-edit">
<div class="preview-edit-picker-preview mr-3">
<img src="" alt="" class="img_preview_edit d-none"
style="width: 120px; height: 90px; object-fit: cover; border-radius: 4px;">
<div class="preview-edit-empty text-muted small text-center">
<i class="fa fa-image d-block mb-1" style="font-size: 24px; opacity: .4;"></i>
Kein Bild<br>ausgewählt
</div>
</div>
<div class="media-body">
<input type="hidden" name="preview_image[url]" id="preview-image-url" value="">
<input type="hidden" name="preview_image[thumb_url]" id="preview-image-thumb-url" value="">
<p class="small text-muted mb-2" id="preview-edit-selected-url">Noch kein Bild ausgewählt.</p>
<button type="button" class="btn btn-secondary btn-sm btn-lfm-open-modal"
data-id="#lfm-preview-edit"
data-obj="preview_image"
data-set='["url", "thumb_url"]'
data-img=".img_preview_edit">
<i class="fa fa-image"></i> Bild aus Mediathek wählen
</button>
<button type="button" class="btn btn-success btn-sm ml-2" id="preview-edit-save-btn" disabled>
<i class="fa fa-save"></i> Seitenbild speichern
</button>
</div>
</div>
</div>
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-default" data-dismiss="modal">Schließen</button>
</div>
</div>
</div>
</div>
<!-- Styles -->
<style>
.tree-node {
margin-left: 0;
padding: 5px 0;
}
.tree-node-content {
display: flex;
align-items: center;
padding: 8px 12px;
border-radius: 4px;
background-color: #fff;
transition: background-color 0.2s;
cursor: pointer;
}
.tree-node-content:hover {
background-color: #f8f9fa;
}
.tree-toggle {
width: 20px;
min-width: 20px;
height: 20px;
display: inline-flex;
align-items: center;
justify-content: center;
cursor: pointer;
margin-right: 8px;
color: #666;
user-select: none;
}
.tree-toggle:hover {
color: #000;
}
.tree-toggle.empty {
visibility: hidden;
}
.tree-icon {
width: 24px;
min-width: 24px;
margin-right: 8px;
text-align: center;
}
.tree-thumb-wrap {
margin-right: 8px;
flex-shrink: 0;
}
.tree-thumb {
width: 44px;
height: 44px;
border-radius: 4px;
object-fit: cover;
display: block;
opacity: 0.88;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.12);
border: 2px solid transparent;
}
.tree-thumb-missing {
opacity: 0.45;
filter: grayscale(1);
border-color: #dc3545;
}
.tree-thumb-box-image {
border-color: #28a745;
}
.tree-thumb-travel-program {
border-color: #007bff;
}
.badge-preview-box {
background-color: #28a745;
color: #fff;
}
.badge-preview-travel {
background-color: #007bff;
color: #fff;
}
.tree-thumbs {
display: flex;
gap: 4px;
margin-right: 8px;
flex-shrink: 0;
}
.tree-edit-preview {
width: 28px;
min-width: 28px;
height: 28px;
padding: 0;
margin-right: 6px;
display: inline-flex;
align-items: center;
justify-content: center;
flex-shrink: 0;
}
.preview-edit-picker-preview {
width: 120px;
min-width: 120px;
height: 90px;
border: 1px dashed #ced4da;
border-radius: 4px;
background: #f8f9fa;
display: flex;
align-items: center;
justify-content: center;
overflow: hidden;
position: relative;
}
.preview-edit-picker-preview .img_preview_edit {
position: absolute;
inset: 0;
width: 100%;
height: 100%;
margin: 0;
}
.preview-edit-empty {
line-height: 1.3;
padding: 8px;
}
#modal-preview-edit .modal-body {
overflow-x: hidden;
}
#modal-preview-edit .preview-edit-current-images {
display: flex;
flex-wrap: wrap;
align-items: flex-start;
gap: 16px;
max-width: 100%;
}
#modal-preview-edit .preview-edit-current-item {
display: flex;
flex-direction: column;
align-items: stretch;
width: 140px;
flex: 0 0 140px;
max-width: 140px;
}
#modal-preview-edit .preview-edit-current-item .badge {
display: block !important;
width: 100%;
margin: 0 0 6px 0;
white-space: normal;
line-height: 1.25;
text-align: left;
position: static;
}
#modal-preview-edit .preview-edit-current-thumb-wrap {
width: 140px;
height: 90px;
max-width: 140px;
max-height: 90px;
border-radius: 4px;
overflow: hidden;
background: #f8f9fa;
flex-shrink: 0;
}
#modal-preview-edit .preview-edit-current-thumb-wrap.preview-edit-border-box {
border: 2px solid #28a745;
}
#modal-preview-edit .preview-edit-current-thumb-wrap.preview-edit-border-travel {
border: 2px solid #007bff;
}
#modal-preview-edit .preview-edit-current-thumb-wrap img {
width: 140px !important;
height: 90px !important;
max-width: 140px !important;
max-height: 90px !important;
object-fit: cover !important;
display: block !important;
margin: 0 !important;
padding: 0 !important;
border: 0 !important;
}
.tree-title {
flex: 1;
font-weight: 500;
}
.tree-badges {
display: flex;
gap: 5px;
flex-wrap: wrap;
}
.tree-url {
font-size: 12px;
color: #212529;
margin-left: 8px;
}
.tree-url a,
.tree-url-link {
color: #212529;
text-decoration: none;
}
.tree-url a:hover,
.tree-url-link:hover {
color: #000;
text-decoration: underline;
}
.tree-children {
margin-left: 30px;
border-left: 2px solid #e0e0e0;
padding-left: 10px;
}
.tree-children.collapsed {
display: none;
}
.tree-node.inactive .tree-node-content {
opacity: 0.5;
}
.tree-node.filtered {
display: none;
}
.tree-node.highlight .tree-node-content {
background-color: #fff3cd;
border-left: 3px solid #ffc107;
}
.badge-small {
font-size: 10px;
padding: 2px 6px;
}
/* Separator-Styles */
.tree-separator {
margin: 15px 0 10px 30px;
padding: 8px 12px;
background: linear-gradient(to right, #f8f9fa 0%, #e9ecef 100%);
border-left: 4px solid #6c757d;
border-radius: 4px;
font-weight: bold;
color: #495057;
}
.tree-separator i {
margin-right: 8px;
color: #6c757d;
}
/* Section-Separator (größere Trennung) */
.tree-section-separator {
margin: 30px 0 20px 0;
padding: 12px 20px;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
border-radius: 6px;
font-weight: bold;
font-size: 18px;
color: white;
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
}
.tree-section-separator i {
margin-right: 10px;
}
/* Frontend-spezifische Styles */
.tree-node.country-page>.tree-node-content {
background-color: #f8f9fa;
font-weight: 600;
font-size: 16px;
border-left: 4px solid #007bff;
padding-left: 16px;
}
.tree-node.country-page>.tree-node-content:hover {
background-color: #e9ecef;
}
.tree-node.info-page>.tree-node-content {
background-color: #fff9e6;
}
.tree-node.sitemap-homepage>.tree-node-content {
background-color: #fff;
color: #212529;
font-weight: bold;
font-size: 16px;
border-left: 4px solid #667eea;
border-radius: 4px;
}
.tree-node.sitemap-homepage .tree-url,
.tree-node.sitemap-homepage .tree-url a {
color: #212529;
}
.tree-node.sitemap-country>.tree-node-content {
font-weight: 600;
border-left: 3px solid #007bff;
}
.tree-node.missing-preview>.tree-node-content {
border-left: 3px solid #dc3545;
}
</style>
<!-- Scripts -->
<script>
let navigationData = null;
let currentFilter = 'active_only';
let currentView = 'sitemap';
let searchQuery = '';
function showInactive() {
return currentFilter === 'with_inactive';
}
function previewSourceThumbClass(source) {
if (source === 'box_image_url' || source === 'box_body') {
return 'tree-thumb-box-image';
}
if (source === 'travel_program_image' || source === 'travel_program_image_fallback') {
return 'tree-thumb-travel-program';
}
return '';
}
function previewSourceBadgeClass(source) {
if (source === 'box_image_url' || source === 'box_body') {
return 'badge-preview-box';
}
if (source === 'travel_program_image' || source === 'travel_program_image_fallback') {
return 'badge-preview-travel';
}
return 'badge-secondary';
}
function loadStats() {
fetch('/navigation-api/stats')
.then(response => response.json())
.then(data => {
if (data.success) {
$('#stat-total').text(data.data.total_pages);
$('#stat-active').text(data.data.active_nodes);
$('#stat-programs').text(data.data.travel_programs);
$('#stat-countries').text(data.data.country_pages);
$('#stat-missing-preview').text(data.data.missing_preview_images);
}
})
.catch(error => console.error('Error loading stats:', error));
}
function loadNavigationTree(includeInactive = false) {
$('#loading').show();
$('#tree-container').hide();
$('#no-results').hide();
const url = `/navigation-api/data?mode=${currentView}&include_hidden=${includeInactive ? '1' : '0'}`;
fetch(url)
.then(response => response.json())
.then(data => {
if (data.success) {
navigationData = data.data;
renderTree(navigationData);
updateNodeCount(data.meta.total_nodes, data.meta.missing_preview_images);
$('#loading').hide();
$('#tree-container').show();
}
})
.catch(error => {
console.error('Error loading tree:', error);
$('#loading').hide();
alert('Fehler beim Laden des Navigationsbaums');
});
}
function renderTree(nodes) {
const container = $('#navigation-tree');
container.empty();
if (nodes.length === 0) {
$('#tree-container').hide();
$('#no-results').show();
return;
}
nodes.forEach(node => {
if (currentView === 'header') {
if (node.is_section_separator || node.is_separator) {
container.append(renderNode(node, false));
} else {
const isCountryPage = node.section === 'Länder-Navigation';
container.append(renderNode(node, isCountryPage));
}
} else {
container.append(renderNode(node, true));
}
});
}
function renderNode(node, expandChildren = true) {
if (node.is_section_separator) {
return $('<div>')
.addClass('tree-section-separator')
.html(`<i class="${node.icon}"></i>${node.title}`);
}
if (node.is_separator) {
return $('<div>')
.addClass('tree-separator')
.html(`<i class="${node.icon}"></i>${node.title}`);
}
const hasChildren = node.has_children;
const isActive = node.status === 1;
const showInNavi = node.show_in_navi === 1;
const isSitemap = currentView === 'sitemap';
const isVirtual = node.is_virtual === true;
const isCountryPage = isSitemap
? node.node_type === 'country_root'
: node.section === 'Länder-Navigation';
const nodeDiv = $('<div>')
.addClass('tree-node')
.addClass(isActive || isVirtual ? 'active' : 'inactive')
.addClass(isCountryPage ? 'country-page sitemap-country' : '')
.addClass(node.before_title === 'Infos' ? 'info-page' : '')
.addClass(isVirtual ? 'sitemap-homepage' : '')
.addClass(isSitemap && node.has_preview_image === false ? 'missing-preview' : '')
.data('node', node);
const contentDiv = $('<div>').addClass('tree-node-content');
const showToggle = hasChildren && (isSitemap || isCountryPage);
const toggle = $('<span>')
.addClass('tree-toggle')
.addClass(showToggle ? '' : 'empty')
.html(showToggle ? '<i class="fa fa-chevron-down"></i>' : '')
.on('click', function(e) {
e.stopPropagation();
toggleNode(nodeDiv);
});
const icon = $('<span>').addClass('tree-icon');
if (isVirtual) {
icon.html('<i class="fa fa-home"></i>');
} else if (node.node_type === 'country_root' || node.is_country_page) {
icon.html('<i class="fa fa-star text-warning"></i>');
} else if (node.node_type === 'category') {
icon.html('<i class="fa fa-folder text-warning"></i>');
} else if (node.is_travel_program || node.node_type === 'travel_program') {
icon.html('<i class="fa fa-plane text-primary"></i>');
} else if (node.is_fewo_lodging || node.node_type === 'fewo_lodging') {
icon.html('<i class="fa fa-home text-info"></i>');
} else if (node.before_title === 'Infos') {
icon.html('<i class="fa fa-info-circle text-muted"></i>');
} else {
icon.html('<i class="fa fa-file-alt text-muted"></i>');
}
const titleDiv = $('<div>').addClass('tree-title');
const displayTitle = node.title_short || node.title;
titleDiv.append($('<span>').text(displayTitle));
if (node.frontend_url || node.url) {
const linkUrl = node.frontend_url || node.url;
const urlSpan = $('<span>').addClass('tree-url');
urlSpan.append(
$('<a>')
.addClass('tree-url-link')
.attr('href', linkUrl)
.attr('target', '_blank')
.attr('rel', 'noopener noreferrer')
.text(linkUrl)
);
titleDiv.append(urlSpan);
}
const badges = $('<div>').addClass('tree-badges');
if (isSitemap && node.node_type_label) {
badges.append($('<span>').addClass('badge badge-secondary badge-small').text(node.node_type_label));
}
if (isSitemap && node.lvl !== undefined && node.lvl !== null && !isVirtual) {
badges.append($('<span>').addClass('badge badge-light badge-small').text('lvl ' + node.lvl));
}
if (!isActive && !isVirtual) {
badges.append($('<span>').addClass('badge badge-danger badge-small').text('Inaktiv'));
}
if (!showInNavi && !isVirtual) {
badges.append($('<span>').addClass('badge badge-warning badge-small').text('Ausgeblendet'));
}
if (isSitemap && node.has_preview_image === false) {
badges.append($('<span>').addClass('badge badge-danger badge-small').text('Kein Vorschaubild'));
}
if (isSitemap && node.has_preview_image === true && node.preview_image_source) {
badges.append(
$('<span>')
.addClass('badge badge-small ' + previewSourceBadgeClass(node.preview_image_source))
.text(node.preview_image_source)
);
}
if (isSitemap && node.travel_program_preview_url &&
['box_image_url', 'box_body'].includes(node.preview_image_source)) {
badges.append(
$('<span>')
.addClass('badge badge-small badge-preview-travel')
.text('travel_program_image')
);
}
if (!isSitemap && node.is_travel_program) {
badges.append($('<span>').addClass('badge badge-primary badge-small').text('Reiseprogramm'));
}
if (!isSitemap && node.is_fewo_lodging) {
badges.append($('<span>').addClass('badge badge-info badge-small').text('FeWo'));
}
if (!isSitemap && node.is_country_page) {
badges.append($('<span>').addClass('badge badge-success badge-small').text('Land'));
}
if (node.before_title) {
badges.append($('<span>').addClass('badge badge-secondary badge-small').text('Gruppe: ' + node.before_title));
}
const contentParts = [toggle];
if (isSitemap && !isVirtual && node.id) {
const editBtn = $('<button type="button" class="btn btn-outline-secondary btn-sm tree-edit-preview">')
.attr('title', 'Vorschaubild bearbeiten')
.html('<i class="fa fa-edit"></i>')
.on('click', function(e) {
e.stopPropagation();
openPreviewEditModal(node);
});
contentParts.push(editBtn);
}
const thumbUrls = [];
if (node.preview_image_url) {
const primaryCls = node.has_preview_image === false
? 'tree-thumb-missing'
: previewSourceThumbClass(node.preview_image_source);
thumbUrls.push({
url: node.preview_image_url,
fallback: null,
cls: primaryCls,
title: node.preview_image_source || 'Vorschaubild',
});
}
if (node.travel_program_preview_url &&
['box_image_url', 'box_body'].includes(node.preview_image_source)) {
thumbUrls.push({
url: node.travel_program_preview_url,
fallback: node.travel_program_preview_url_full || null,
cls: 'tree-thumb-travel-program',
title: 'Reiseprogramm-Bild (v2)',
});
}
if (thumbUrls.length > 0) {
const thumbsWrap = $('<span>').addClass('tree-thumbs');
thumbUrls.forEach(function(thumbData) {
const thumbWrap = $('<span>').addClass('tree-thumb-wrap').attr('title', thumbData.title);
const thumb = $('<img>')
.addClass('tree-thumb')
.addClass(thumbData.cls)
.attr('src', thumbData.url)
.attr('alt', '')
.attr('loading', 'lazy');
if (thumbData.fallback) {
thumb.attr('data-fallback-url', thumbData.fallback);
}
thumb.on('error', function() {
const fallbackUrl = $(this).attr('data-fallback-url');
if (fallbackUrl && !$(this).data('fallback-tried')) {
$(this).data('fallback-tried', true).attr('src', fallbackUrl);
return;
}
$(this).closest('.tree-thumb-wrap').remove();
if (thumbsWrap.find('.tree-thumb-wrap').length === 0) {
thumbsWrap.remove();
}
});
thumbWrap.append(thumb);
thumbsWrap.append(thumbWrap);
});
contentParts.push(thumbsWrap);
}
contentParts.push(icon, titleDiv, badges);
contentDiv.append(contentParts);
nodeDiv.append(contentDiv);
if (hasChildren && node.children && node.children.length > 0) {
const shouldRenderChildren = isSitemap || isCountryPage;
if (shouldRenderChildren) {
const childrenDiv = $('<div>').addClass('tree-children');
if (!expandChildren) {
childrenDiv.addClass('collapsed');
}
node.children.forEach(child => {
childrenDiv.append(renderNode(child, expandChildren));
});
nodeDiv.append(childrenDiv);
}
}
return nodeDiv;
}
// Toggle Node
function toggleNode(nodeDiv) {
const childrenDiv = nodeDiv.find('> .tree-children');
const toggle = nodeDiv.find('> .tree-node-content > .tree-toggle i');
if (childrenDiv.hasClass('collapsed')) {
childrenDiv.removeClass('collapsed');
toggle.removeClass('fa-chevron-right').addClass('fa-chevron-down');
} else {
childrenDiv.addClass('collapsed');
toggle.removeClass('fa-chevron-down').addClass('fa-chevron-right');
}
}
function performSearch(query) {
searchQuery = query.toLowerCase();
if (!query) {
$('.tree-node').removeClass('highlight filtered');
return;
}
$('.tree-node').each(function() {
const node = $(this).data('node');
const matches =
node.title.toLowerCase().includes(searchQuery) ||
(node.slug && node.slug.toLowerCase().includes(searchQuery)) ||
(node.url && node.url.toLowerCase().includes(searchQuery));
if (matches) {
$(this).removeClass('filtered').addClass('highlight');
$(this).parents('.tree-children').removeClass('collapsed');
$(this).parents('.tree-node').find('> .tree-node-content > .tree-toggle i')
.removeClass('fa-chevron-right').addClass('fa-chevron-down');
} else {
$(this).removeClass('highlight');
}
});
}
function updateNodeCount(count, missingPreview) {
let label = `${count} Knoten`;
if (currentView === 'sitemap' && missingPreview !== undefined) {
label += ` · ${missingPreview} ohne Vorschaubild`;
}
$('#node-count').text(label);
}
let previewEditNode = null;
function resetPreviewEditPicker() {
$('#preview-image-url').val('');
$('#preview-image-thumb-url').val('');
$('#preview-edit-selected-url').text('Noch kein Bild ausgewählt.');
$('#preview-edit-save-btn').prop('disabled', true);
$('.img_preview_edit').attr('src', '').addClass('d-none');
$('.preview-edit-empty').show();
}
function showPreviewEditPickerImage(url) {
if (!url) {
resetPreviewEditPicker();
return;
}
$('.img_preview_edit').attr('src', url).removeClass('d-none');
$('.preview-edit-empty').hide();
$('#preview-edit-selected-url').text(url);
$('#preview-edit-save-btn').prop('disabled', false);
}
function openPreviewEditModal(node) {
previewEditNode = node;
$('#preview-edit-page-title').text(node.title_short || node.title);
$('#preview-edit-page-url').text(node.frontend_url || node.url || '');
const $current = $('#preview-edit-current-images').empty();
if (node.box_body_preview_url) {
renderPreviewEditCurrentImage($current, 'Legacy-Bild (box_body)', node.box_body_preview_url, 'preview-edit-border-box', 'badge-preview-box');
}
renderPreviewEditCurrentImage($current, 'Seitenbild (box_image_url)', node.box_image_frontend_url, 'preview-edit-border-box', 'badge-preview-box');
renderPreviewEditCurrentImage($current, 'Reiseprogramm-Bild', node.travel_program_preview_url || node.travel_program_preview_url_full, 'preview-edit-border-travel', 'badge-preview-travel');
if (!$current.children().length) {
$current.append('<div class="text-muted small w-100">Aktuell ist kein Vorschaubild hinterlegt.</div>');
}
$('#preview-edit-delete-boxbody-section').toggle(!!node.can_clear_box_body);
$('#preview-edit-delete-section').toggle(!!node.can_clear_box_image);
resetPreviewEditPicker();
$('#modal-preview-edit').modal('show');
}
function renderPreviewEditCurrentImage($container, label, url, borderClass, badgeClass) {
if (!url) {
return;
}
const $item = $('<div class="preview-edit-current-item">');
$item.append(
$('<span>')
.addClass('badge badge-small ' + badgeClass)
.text(label)
);
const $wrap = $('<div class="preview-edit-current-thumb-wrap">').addClass(borderClass);
$wrap.append(
$('<img>')
.attr('src', url)
.attr('alt', label)
);
$item.append($wrap);
$container.append($item);
}
function savePreviewBoxImage(action, boxImageUrl) {
if (!previewEditNode || !previewEditNode.id) {
return;
}
const token = $('meta[name="csrf-token"]').attr('content');
const body = { action: action };
if (boxImageUrl) {
body.box_image_url = boxImageUrl;
}
fetch('/navigation-api/page/' + previewEditNode.id + '/box-image', {
method: 'POST',
headers: {
'X-CSRF-TOKEN': token,
'Accept': 'application/json',
'Content-Type': 'application/json',
},
body: JSON.stringify(body),
})
.then(response => response.json().then(data => ({ ok: response.ok, data })))
.then(({ ok, data }) => {
if (!ok || !data.success) {
throw new Error(data.error || 'Speichern fehlgeschlagen.');
}
$.growl({
title: 'Gespeichert',
message: data.message,
location: 'tr',
});
$('#modal-preview-edit').modal('hide');
loadStats();
loadNavigationTree(showInactive());
})
.catch(error => {
$.growl.error({
title: 'Fehler',
message: error.message,
location: 'tr',
});
});
}
function clearCache() {
if (!confirm('Möchten Sie wirklich den Navigation-Cache leeren?')) {
return;
}
const token = $('meta[name="csrf-token"]').attr('content');
fetch('/navigation-api/clear-cache', {
method: 'POST',
headers: {
'X-CSRF-TOKEN': token,
'Accept': 'application/json'
}
})
.then(() => {
alert('Cache erfolgreich gelöscht');
loadStats();
loadNavigationTree(showInactive());
})
.catch(error => {
console.error('Error clearing cache:', error);
alert('Fehler beim Löschen des Cache');
});
}
function switchView(view) {
currentView = view;
$('#tree-view-tabs .nav-link').removeClass('active');
$(`#tree-view-tabs .nav-link[data-view="${view}"]`).addClass('active');
$('#view-info-sitemap').toggle(view === 'sitemap');
$('#view-info-header').toggle(view === 'header');
loadNavigationTree(showInactive());
}
$(document).ready(function() {
if (typeof IqLFileManager !== 'undefined') {
IqLFileManager.image_dialog_images_url = '/navigation-api/iq-assets/modal';
IqLFileManager.init();
}
loadStats();
loadNavigationTree(false);
$('#preview-edit-delete-btn').on('click', function() {
if (!previewEditNode || !previewEditNode.can_clear_box_image) {
return;
}
if (!confirm('Möchten Sie das verknüpfte Seitenbild (box_image_url) wirklich entfernen?')) {
return;
}
savePreviewBoxImage('clear');
});
$('#preview-edit-delete-boxbody-btn').on('click', function() {
if (!previewEditNode || !previewEditNode.can_clear_box_body) {
return;
}
if (!confirm('Möchten Sie das Legacy-Bild (box_body) wirklich entfernen?')) {
return;
}
savePreviewBoxImage('clear_box_body');
});
$('#preview-edit-save-btn').on('click', function() {
const url = $('#preview-image-url').val();
if (!url) {
$.growl.error({
title: 'Hinweis',
message: 'Bitte wählen Sie zuerst ein Bild aus der Mediathek.',
location: 'tr',
});
return;
}
savePreviewBoxImage('set', url);
});
$('#preview-image-url').on('change', function() {
const url = $(this).val();
const thumbUrl = $('#preview-image-thumb-url').val();
showPreviewEditPickerImage(thumbUrl || url);
});
$('#tree-view-tabs .nav-link').on('click', function(e) {
e.preventDefault();
switchView($(this).data('view'));
});
$('#clear-cache-btn').on('click', clearCache);
$('#search-btn').on('click', function() {
performSearch($('#search-input').val());
});
$('#search-input').on('keyup', function(e) {
if (e.key === 'Enter') {
performSearch($(this).val());
}
});
$('#filter-inactive-btn').on('click', function() {
if (currentFilter === 'active_only') {
currentFilter = 'with_inactive';
$(this).html('<i class="fa fa-eye-slash"></i> Inaktive ausblenden')
.removeClass('btn-outline-secondary')
.addClass('btn-secondary');
} else {
currentFilter = 'active_only';
$(this).html('<i class="fa fa-eye"></i> Inaktive einblenden')
.removeClass('btn-secondary')
.addClass('btn-outline-secondary');
}
loadNavigationTree(showInactive());
});
});
</script>
@endsection