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>
1151 lines
44 KiB
PHP
1151 lines
44 KiB
PHP
@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
|