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>
This commit is contained in:
parent
6df9c401af
commit
c39d86f80b
6 changed files with 1305 additions and 136 deletions
|
|
@ -2,9 +2,11 @@
|
|||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use App\Models\Page;
|
||||
use App\Services\NavigationTreeService;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use IqContent\LaravelFilemanager\Lfm;
|
||||
|
||||
class NavigationTreeController extends Controller
|
||||
{
|
||||
|
|
@ -22,7 +24,9 @@ class NavigationTreeController extends Controller
|
|||
*/
|
||||
public function index()
|
||||
{
|
||||
return view('navigation.index');
|
||||
return view('navigation.index', [
|
||||
'lfm_helper' => app(Lfm::class),
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -34,7 +38,25 @@ class NavigationTreeController extends Controller
|
|||
public function getData(Request $request): JsonResponse
|
||||
{
|
||||
try {
|
||||
$includeHidden = $request->get('include_hidden', true);
|
||||
$mode = $request->get('mode', 'sitemap');
|
||||
$includeHidden = $request->get('include_hidden', false);
|
||||
$onlyActive = !$includeHidden;
|
||||
|
||||
if ($mode === 'sitemap') {
|
||||
$tree = $this->navigationService->getSitemapTree($onlyActive);
|
||||
|
||||
return response()->json([
|
||||
'success' => true,
|
||||
'data' => $tree,
|
||||
'meta' => [
|
||||
'total_nodes' => $this->navigationService->countNodes($tree),
|
||||
'missing_preview_images' => $this->navigationService->countMissingPreviewImages($tree),
|
||||
'include_hidden' => $includeHidden,
|
||||
'structure' => 'sitemap',
|
||||
],
|
||||
]);
|
||||
}
|
||||
|
||||
$tree = $this->navigationService->getFrontendNavigationTree($includeHidden);
|
||||
|
||||
return response()->json([
|
||||
|
|
@ -43,8 +65,8 @@ class NavigationTreeController extends Controller
|
|||
'meta' => [
|
||||
'total_nodes' => $this->navigationService->countNodes($tree),
|
||||
'include_hidden' => $includeHidden,
|
||||
'structure' => 'frontend'
|
||||
]
|
||||
'structure' => 'header',
|
||||
],
|
||||
]);
|
||||
} catch (\Exception $e) {
|
||||
return response()->json([
|
||||
|
|
@ -98,10 +120,17 @@ class NavigationTreeController extends Controller
|
|||
public function export(Request $request)
|
||||
{
|
||||
try {
|
||||
$includeHidden = $request->get('include_hidden', true);
|
||||
$tree = $this->navigationService->getFrontendNavigationTree($includeHidden);
|
||||
$mode = $request->get('mode', 'sitemap');
|
||||
$includeHidden = $request->get('include_hidden', false);
|
||||
$onlyActive = !$includeHidden;
|
||||
|
||||
$filename = 'navigation-tree-frontend-' . date('Y-m-d-His') . '.json';
|
||||
if ($mode === 'sitemap') {
|
||||
$tree = $this->navigationService->getSitemapTree($onlyActive);
|
||||
$filename = 'sitemap-tree-' . date('Y-m-d-His') . '.json';
|
||||
} else {
|
||||
$tree = $this->navigationService->getFrontendNavigationTree($includeHidden);
|
||||
$filename = 'navigation-tree-frontend-' . date('Y-m-d-His') . '.json';
|
||||
}
|
||||
$json = json_encode($tree, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
|
||||
|
||||
return response($json)
|
||||
|
|
@ -117,12 +146,21 @@ class NavigationTreeController extends Controller
|
|||
*
|
||||
* @return \Illuminate\Http\RedirectResponse
|
||||
*/
|
||||
public function clearCache()
|
||||
public function clearCache(Request $request)
|
||||
{
|
||||
try {
|
||||
$this->navigationService->clearCache();
|
||||
|
||||
if ($request->expectsJson() || $request->ajax()) {
|
||||
return response()->json(['success' => true]);
|
||||
}
|
||||
|
||||
return back()->with('success', 'Navigation-Cache erfolgreich gelöscht');
|
||||
} catch (\Exception $e) {
|
||||
if ($request->expectsJson() || $request->ajax()) {
|
||||
return response()->json(['success' => false, 'error' => $e->getMessage()], 500);
|
||||
}
|
||||
|
||||
return back()->with('error', 'Cache-Löschung fehlgeschlagen: ' . $e->getMessage());
|
||||
}
|
||||
}
|
||||
|
|
@ -149,6 +187,9 @@ class NavigationTreeController extends Controller
|
|||
'travel_programs' => count(array_filter($flatList, fn($n) => $n['is_travel_program'])),
|
||||
'fewo_lodgings' => count(array_filter($flatList, fn($n) => $n['is_fewo_lodging'])),
|
||||
'country_pages' => count(array_filter($flatList, fn($n) => $n['is_country_page'])),
|
||||
'missing_preview_images' => $this->navigationService->countMissingPreviewImages(
|
||||
$this->navigationService->getSitemapTree(false)
|
||||
),
|
||||
];
|
||||
|
||||
return response()->json([
|
||||
|
|
@ -162,4 +203,76 @@ class NavigationTreeController extends Controller
|
|||
], 500);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Setzt oder löscht box_image_url einer Seite.
|
||||
*/
|
||||
public function updateBoxImage(Request $request, int $id): JsonResponse
|
||||
{
|
||||
try {
|
||||
$page = Page::findOrFail($id);
|
||||
$action = $request->input('action');
|
||||
|
||||
if ($action === 'clear') {
|
||||
if (empty($page->box_image_url)) {
|
||||
return response()->json([
|
||||
'success' => false,
|
||||
'error' => 'Für diese Seite ist kein verknüpftes Seitenbild (box_image_url) hinterlegt.',
|
||||
], 422);
|
||||
}
|
||||
|
||||
if (!$this->navigationService->pageCanClearBoxImage($page)) {
|
||||
return response()->json([
|
||||
'success' => false,
|
||||
'error' => 'Das Seitenbild kann hier nicht entfernt werden, weil kein Reiseprogramm-Bild als Ersatz vorhanden ist.',
|
||||
], 422);
|
||||
}
|
||||
|
||||
$page->box_image_url = null;
|
||||
$page->save();
|
||||
} elseif ($action === 'clear_box_body') {
|
||||
if (!$this->navigationService->pageHasBoxBodyPreview($page)) {
|
||||
return response()->json([
|
||||
'success' => false,
|
||||
'error' => 'Für diese Seite ist kein Legacy-Bild (box_body) hinterlegt.',
|
||||
], 422);
|
||||
}
|
||||
|
||||
$page->box_body = null;
|
||||
$page->save();
|
||||
} elseif ($action === 'set') {
|
||||
$url = trim((string) $request->input('box_image_url', ''));
|
||||
if ($url === '') {
|
||||
return response()->json([
|
||||
'success' => false,
|
||||
'error' => 'Bitte wählen Sie zuerst ein Bild aus der Mediathek.',
|
||||
], 422);
|
||||
}
|
||||
|
||||
$page->box_image_url = $url;
|
||||
$page->save();
|
||||
} else {
|
||||
return response()->json([
|
||||
'success' => false,
|
||||
'error' => 'Ungültige Aktion.',
|
||||
], 422);
|
||||
}
|
||||
|
||||
$this->navigationService->clearCache();
|
||||
|
||||
return response()->json([
|
||||
'success' => true,
|
||||
'message' => match ($action) {
|
||||
'clear' => 'Das verknüpfte Seitenbild wurde entfernt.',
|
||||
'clear_box_body' => 'Das Legacy-Bild (box_body) wurde entfernt.',
|
||||
default => 'Das neue Seitenbild wurde gespeichert.',
|
||||
},
|
||||
]);
|
||||
} catch (\Exception $e) {
|
||||
return response()->json([
|
||||
'success' => false,
|
||||
'error' => $e->getMessage(),
|
||||
], 500);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ namespace App\Services;
|
|||
use App\Models\Page;
|
||||
use Illuminate\Support\Collection;
|
||||
use Illuminate\Support\Facades\Cache;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
|
||||
class NavigationTreeService
|
||||
{
|
||||
|
|
@ -20,6 +21,14 @@ class NavigationTreeService
|
|||
*/
|
||||
protected $cacheEnabled = true;
|
||||
|
||||
/** @var array<int, object> */
|
||||
protected $travelProgramImageMap = [];
|
||||
|
||||
/** @var array<int, object> */
|
||||
protected $fewoImageMap = [];
|
||||
|
||||
protected $previewImagesPreloaded = false;
|
||||
|
||||
/**
|
||||
* Aktiviert oder deaktiviert Caching
|
||||
*
|
||||
|
|
@ -55,6 +64,10 @@ class NavigationTreeService
|
|||
Cache::forget('navigation_tree_active');
|
||||
Cache::forget('navigation_flat_full');
|
||||
Cache::forget('navigation_flat_active');
|
||||
Cache::forget('navigation_tree_sitemap_full');
|
||||
Cache::forget('navigation_tree_sitemap_active');
|
||||
Cache::forget('navigation_tree_frontend_with_hidden');
|
||||
Cache::forget('navigation_tree_frontend_visible');
|
||||
|
||||
// Lösche auch alle Subtree-Caches
|
||||
$keys = Cache::get('navigation_subtree_keys', []);
|
||||
|
|
@ -102,6 +115,421 @@ class NavigationTreeService
|
|||
return $this->buildFrontendNavigationTree($includeHidden);
|
||||
}
|
||||
|
||||
/**
|
||||
* Vollständiger Seiten-Sitemap-Baum (Nested Set), von Root bis Reiseprogramm.
|
||||
*
|
||||
* @param bool $onlyActive
|
||||
* @return array
|
||||
*/
|
||||
public function getSitemapTree(bool $onlyActive = false): array
|
||||
{
|
||||
$cacheKey = $onlyActive ? 'navigation_tree_sitemap_active' : 'navigation_tree_sitemap_full';
|
||||
|
||||
if ($this->cacheEnabled) {
|
||||
return Cache::remember($cacheKey, $this->cacheTime, function () use ($onlyActive) {
|
||||
return $this->buildSitemapTree($onlyActive);
|
||||
});
|
||||
}
|
||||
|
||||
return $this->buildSitemapTree($onlyActive);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param bool $onlyActive
|
||||
* @return array
|
||||
*/
|
||||
protected function buildSitemapTree(bool $onlyActive = false): array
|
||||
{
|
||||
$this->preloadPreviewImageMaps();
|
||||
$roots = $this->getNestedSetRootPages($onlyActive);
|
||||
|
||||
$children = [];
|
||||
foreach ($roots as $rootPage) {
|
||||
$children[] = $this->buildSitemapNode($rootPage, $onlyActive);
|
||||
}
|
||||
|
||||
return [
|
||||
[
|
||||
'id' => 'homepage',
|
||||
'title' => 'Startseite',
|
||||
'slug' => null,
|
||||
'url' => '/',
|
||||
'frontend_url' => $this->buildFrontendUrl('/'),
|
||||
'status' => 1,
|
||||
'show_in_navi' => 1,
|
||||
'node_type' => 'homepage',
|
||||
'node_type_label' => 'Startseite',
|
||||
'is_virtual' => true,
|
||||
'has_preview_image' => null,
|
||||
'preview_image_source' => null,
|
||||
'children' => $children,
|
||||
'has_children' => count($children) > 0,
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @param bool $onlyActive
|
||||
* @return \Illuminate\Database\Eloquent\Collection
|
||||
*/
|
||||
protected function getNestedSetRootPages(bool $onlyActive = false)
|
||||
{
|
||||
$query = Page::where('lvl', 0)
|
||||
->orderBy('lft')
|
||||
->orderBy('order')
|
||||
->orderBy('title');
|
||||
|
||||
if ($onlyActive) {
|
||||
$query->where('status', 1);
|
||||
}
|
||||
|
||||
return $query->get();
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Page $page
|
||||
* @param bool $onlyActive
|
||||
* @return array
|
||||
*/
|
||||
protected function buildSitemapNode(Page $page, bool $onlyActive = false): array
|
||||
{
|
||||
$node = $this->buildNodeData($page, true);
|
||||
$node['node_type'] = $this->resolveSitemapNodeType($page);
|
||||
$node['node_type_label'] = $this->resolveSitemapNodeTypeLabel($node['node_type']);
|
||||
$node['is_virtual'] = false;
|
||||
|
||||
if ($this->nodeNeedsPreviewImage($node['node_type'])) {
|
||||
if (empty($node['preview_image_url'])) {
|
||||
$node['preview_image_url'] = $this->getNoPreviewImageUrl();
|
||||
}
|
||||
} else {
|
||||
$node['has_preview_image'] = null;
|
||||
}
|
||||
|
||||
$children = $this->getNestedSetChildren($page, $onlyActive);
|
||||
$childNodes = [];
|
||||
foreach ($children as $child) {
|
||||
$childNodes[] = $this->buildSitemapNode($child, $onlyActive);
|
||||
}
|
||||
|
||||
$node['children'] = $childNodes;
|
||||
$node['has_children'] = count($childNodes) > 0;
|
||||
|
||||
return $node;
|
||||
}
|
||||
|
||||
/**
|
||||
* Direkte Kinder per Nested-Set (wie im Symfony-Frontend).
|
||||
*
|
||||
* @param Page $parent
|
||||
* @param bool $onlyActive
|
||||
* @return \Illuminate\Database\Eloquent\Collection
|
||||
*/
|
||||
protected function getNestedSetChildren(Page $parent, bool $onlyActive = false)
|
||||
{
|
||||
if ($parent->lft === null || $parent->rgt === null || $parent->lvl === null) {
|
||||
return Page::where('parent_id', $parent->id)
|
||||
->orderBy('order')
|
||||
->orderBy('title')
|
||||
->when($onlyActive, function ($query) {
|
||||
$query->where('status', 1);
|
||||
})
|
||||
->get();
|
||||
}
|
||||
|
||||
$query = Page::where('lvl', $parent->lvl + 1)
|
||||
->where('lft', '>', $parent->lft)
|
||||
->where('rgt', '<', $parent->rgt)
|
||||
->when($parent->tree_root !== null, function ($query) use ($parent) {
|
||||
$query->where('tree_root', $parent->tree_root);
|
||||
})
|
||||
->orderBy('lft')
|
||||
->orderBy('order')
|
||||
->orderBy('title');
|
||||
|
||||
if ($onlyActive) {
|
||||
$query->where('status', 1);
|
||||
}
|
||||
|
||||
return $query->get();
|
||||
}
|
||||
|
||||
protected function resolveSitemapNodeType(Page $page): string
|
||||
{
|
||||
if (!empty($page->travel_program)) {
|
||||
return 'travel_program';
|
||||
}
|
||||
|
||||
if (!empty($page->fewo_lodging)) {
|
||||
return 'fewo_lodging';
|
||||
}
|
||||
|
||||
if (!empty($page->travel_guide_content_id)) {
|
||||
return 'travel_guide';
|
||||
}
|
||||
|
||||
if (!empty($page->country_id) && $page->template === 'overview' && (int) $page->lvl === 0) {
|
||||
return 'country_root';
|
||||
}
|
||||
|
||||
if ($page->template === 'overview') {
|
||||
return 'category';
|
||||
}
|
||||
|
||||
if (in_array($page->template, ['travelProgram', 'travelProgramOverview', 'nilecruise'], true)) {
|
||||
return 'program_overview';
|
||||
}
|
||||
|
||||
return 'content';
|
||||
}
|
||||
|
||||
protected function resolveSitemapNodeTypeLabel(string $nodeType): string
|
||||
{
|
||||
$labels = [
|
||||
'homepage' => 'Startseite',
|
||||
'country_root' => 'Land / Reisekategorie',
|
||||
'category' => 'Unterkategorie',
|
||||
'program_overview' => 'Programm-Übersicht',
|
||||
'travel_program' => 'Einzelreise',
|
||||
'fewo_lodging' => 'Ferienwohnung',
|
||||
'travel_guide' => 'Reiseführer',
|
||||
'content' => 'Inhaltsseite',
|
||||
];
|
||||
|
||||
return $labels[$nodeType] ?? 'Seite';
|
||||
}
|
||||
|
||||
protected function nodeNeedsPreviewImage(string $nodeType): bool
|
||||
{
|
||||
return in_array($nodeType, [
|
||||
'country_root',
|
||||
'category',
|
||||
'program_overview',
|
||||
'travel_program',
|
||||
'fewo_lodging',
|
||||
], true);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Page $page
|
||||
* @return array{has_preview_image: bool, preview_image_source: string|null, preview_image_url: string|null}
|
||||
*/
|
||||
protected function resolvePreviewImage(Page $page): array
|
||||
{
|
||||
$this->preloadPreviewImageMaps();
|
||||
|
||||
$boxBodyThumb = $this->getBoxBodyThumbUrl($page->box_body);
|
||||
if ($boxBodyThumb !== null) {
|
||||
return $this->previewImageResult(true, 'box_body', $boxBodyThumb);
|
||||
}
|
||||
|
||||
if (!empty($page->box_image_url)) {
|
||||
return $this->previewImageResult(true, 'box_image_url', $page->box_image_url);
|
||||
}
|
||||
|
||||
if (!empty($page->travel_program)) {
|
||||
$image = $this->travelProgramImageMap[$page->travel_program] ?? null;
|
||||
if ($image !== null) {
|
||||
$source = (int) $image->type === 2
|
||||
? 'travel_program_image'
|
||||
: 'travel_program_image_fallback';
|
||||
|
||||
return [
|
||||
'has_preview_image' => true,
|
||||
'preview_image_source' => $source,
|
||||
'preview_image_url' => $this->buildTravelProgramImageUrl($image, false),
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
if (!empty($page->fewo_lodging)) {
|
||||
$image = $this->fewoImageMap[$page->fewo_lodging] ?? null;
|
||||
if ($image !== null && !empty($image->full_file_name)) {
|
||||
return $this->previewImageResult(
|
||||
true,
|
||||
'fewo_lodging_image',
|
||||
'_uploads/images/' . $image->full_file_name
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return $this->previewImageResult(false, null, null);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param bool $hasPreview
|
||||
* @param string|null $source
|
||||
* @param string|null $path
|
||||
* @return array{has_preview_image: bool, preview_image_source: string|null, preview_image_url: string|null}
|
||||
*/
|
||||
protected function previewImageResult(bool $hasPreview, ?string $source, ?string $path): array
|
||||
{
|
||||
return [
|
||||
'has_preview_image' => $hasPreview,
|
||||
'preview_image_source' => $source,
|
||||
'preview_image_url' => $hasPreview ? $this->buildFrontendAssetUrl($path) : null,
|
||||
];
|
||||
}
|
||||
|
||||
protected function resolveTravelProgramPreviewUrl(Page $page, bool $useThumb = true): ?string
|
||||
{
|
||||
if (empty($page->travel_program)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$this->preloadPreviewImageMaps();
|
||||
|
||||
$image = $this->travelProgramImageMap[$page->travel_program] ?? null;
|
||||
if ($image === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return $this->buildTravelProgramImageUrl($image, $useThumb);
|
||||
}
|
||||
|
||||
public function pageCanClearBoxImage(Page $page): bool
|
||||
{
|
||||
if (empty($page->box_image_url) || empty($page->travel_program)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return $this->resolveTravelProgramPreviewUrl($page, false) !== null;
|
||||
}
|
||||
|
||||
public function pageHasBoxBodyPreview(Page $page): bool
|
||||
{
|
||||
return $this->getBoxBodyThumbUrl($page->box_body) !== null;
|
||||
}
|
||||
|
||||
public function pageCanClearBoxBody(Page $page): bool
|
||||
{
|
||||
return $this->pageHasBoxBodyPreview($page);
|
||||
}
|
||||
|
||||
/**
|
||||
* Reiseprogramm-Bilder liegen auf v2 (nicht sterntours.de).
|
||||
*
|
||||
* @param object $image
|
||||
* @param bool $useThumb
|
||||
* @return string
|
||||
*/
|
||||
protected function buildTravelProgramImageUrl(object $image, bool $useThumb = true): string
|
||||
{
|
||||
$fileName = $image->file_name;
|
||||
if ($useThumb) {
|
||||
$fileName .= '_thumb';
|
||||
}
|
||||
|
||||
$path = 'uploads/travel_program/' . $fileName . $image->extension;
|
||||
|
||||
return rtrim((string) config('app.url_v2'), '/') . '/' . ltrim($path, '/');
|
||||
}
|
||||
|
||||
protected function preloadPreviewImageMaps(): void
|
||||
{
|
||||
if ($this->previewImagesPreloaded) {
|
||||
return;
|
||||
}
|
||||
|
||||
$travelImages = DB::connection('mysql_stern')
|
||||
->table('travel_program_image')
|
||||
->orderBy('id')
|
||||
->get();
|
||||
|
||||
foreach ($travelImages->groupBy('program_id') as $programId => $images) {
|
||||
$preferred = $images->first(function ($image) {
|
||||
return (int) $image->type === 2;
|
||||
});
|
||||
$this->travelProgramImageMap[$programId] = $preferred ?? $images->first();
|
||||
}
|
||||
|
||||
$fewoImages = DB::connection('mysql_stern')
|
||||
->table('fewo_lodging_image')
|
||||
->orderBy('pos')
|
||||
->orderBy('id')
|
||||
->get();
|
||||
|
||||
foreach ($fewoImages->groupBy('lodging_id') as $lodgingId => $images) {
|
||||
$this->fewoImageMap[$lodgingId] = $images->first();
|
||||
}
|
||||
|
||||
$this->previewImagesPreloaded = true;
|
||||
}
|
||||
|
||||
protected function getBoxBodyThumbUrl(?string $boxBody): ?string
|
||||
{
|
||||
if (empty($boxBody)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$decoded = json_decode($boxBody, true);
|
||||
|
||||
if (!is_array($decoded) || empty($decoded['thumb_url'])) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return $decoded['thumb_url'];
|
||||
}
|
||||
|
||||
protected function getNoPreviewImageUrl(): string
|
||||
{
|
||||
return $this->buildFrontendAssetUrl('bundles/app/images/no-picture.png');
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string|null $path
|
||||
* @return string|null
|
||||
*/
|
||||
protected function buildFrontendAssetUrl(?string $path): ?string
|
||||
{
|
||||
if ($path === null || $path === '') {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (preg_match('/^https?:\/\//i', $path)) {
|
||||
return $path;
|
||||
}
|
||||
|
||||
$base = rtrim((string) config('app.url_stern'), '/');
|
||||
|
||||
return $base . (str_starts_with($path, '/') ? $path : '/' . $path);
|
||||
}
|
||||
|
||||
protected function hasBoxBodyPreviewImage(?string $boxBody): bool
|
||||
{
|
||||
return $this->getBoxBodyThumbUrl($boxBody) !== null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Zählt Knoten ohne Vorschaubild im Sitemap-Baum.
|
||||
*
|
||||
* @param array $tree
|
||||
* @return int
|
||||
*/
|
||||
public function countMissingPreviewImages(array $tree): int
|
||||
{
|
||||
$count = 0;
|
||||
|
||||
foreach ($tree as $node) {
|
||||
if (!empty($node['is_virtual'])) {
|
||||
if (!empty($node['children'])) {
|
||||
$count += $this->countMissingPreviewImages($node['children']);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
if (isset($node['has_preview_image']) && $node['has_preview_image'] === false) {
|
||||
$count++;
|
||||
}
|
||||
|
||||
if (!empty($node['children'])) {
|
||||
$count += $this->countMissingPreviewImages($node['children']);
|
||||
}
|
||||
}
|
||||
|
||||
return $count;
|
||||
}
|
||||
|
||||
/**
|
||||
* Baut den Frontend-Navigationsbaum auf
|
||||
*
|
||||
|
|
@ -110,6 +538,7 @@ class NavigationTreeService
|
|||
*/
|
||||
protected function buildFrontendNavigationTree(bool $includeHidden = false): array
|
||||
{
|
||||
$this->preloadPreviewImageMaps();
|
||||
$tree = [];
|
||||
|
||||
// 1. Länderseiten (Hauptnavigation)
|
||||
|
|
@ -492,6 +921,8 @@ class NavigationTreeService
|
|||
*/
|
||||
protected function buildNodeData(Page $page, bool $includeRelations = true): array
|
||||
{
|
||||
$path = $page->real_url_path ?: $this->buildUrlPath($page);
|
||||
|
||||
$data = [
|
||||
'id' => $page->id,
|
||||
'title' => $page->title,
|
||||
|
|
@ -499,7 +930,8 @@ class NavigationTreeService
|
|||
'before_title' => $page->before_title,
|
||||
'slug' => $page->slug,
|
||||
'real_url_path' => $page->real_url_path,
|
||||
'url' => $page->real_url_path ?: $this->buildUrlPath($page),
|
||||
'url' => $path,
|
||||
'frontend_url' => $this->buildFrontendUrl($path),
|
||||
'status' => $page->status,
|
||||
'show_in_navi' => $page->show_in_navi,
|
||||
'order' => $page->order,
|
||||
|
|
@ -521,6 +953,9 @@ class NavigationTreeService
|
|||
// Box-Informationen (für Karten/Boxen in der Navigation)
|
||||
'box_body' => $page->box_body,
|
||||
'box_image_url' => $page->box_image_url,
|
||||
'box_image_frontend_url' => !empty($page->box_image_url)
|
||||
? $this->buildFrontendAssetUrl($page->box_image_url)
|
||||
: null,
|
||||
'box_star' => $page->box_star,
|
||||
'box_discount' => $page->box_discount,
|
||||
|
||||
|
|
@ -583,9 +1018,37 @@ class NavigationTreeService
|
|||
}
|
||||
}
|
||||
|
||||
$preview = $this->resolvePreviewImage($page);
|
||||
$data['has_preview_image'] = $preview['has_preview_image'];
|
||||
$data['preview_image_source'] = $preview['preview_image_source'];
|
||||
$data['preview_image_url'] = $preview['preview_image_url'];
|
||||
$data['travel_program_preview_url'] = $this->resolveTravelProgramPreviewUrl($page, true);
|
||||
$data['travel_program_preview_url_full'] = $this->resolveTravelProgramPreviewUrl($page, false);
|
||||
$data['can_clear_box_image'] = $this->pageCanClearBoxImage($page);
|
||||
$data['has_box_body_preview'] = $this->pageHasBoxBodyPreview($page);
|
||||
$data['box_body_preview_url'] = $this->getBoxBodyThumbUrl($page->box_body);
|
||||
$data['can_clear_box_body'] = $this->pageCanClearBoxBody($page);
|
||||
|
||||
return $data;
|
||||
}
|
||||
|
||||
/**
|
||||
* Baut die vollständige Frontend-URL für eine Seite.
|
||||
*
|
||||
* @param string|null $path
|
||||
* @return string|null
|
||||
*/
|
||||
protected function buildFrontendUrl(?string $path): ?string
|
||||
{
|
||||
if ($path === null || $path === '') {
|
||||
return null;
|
||||
}
|
||||
|
||||
$base = rtrim((string) config('app.url_stern'), '/');
|
||||
|
||||
return $base . ($path === '/' ? '/' : (str_starts_with($path, '/') ? $path : '/' . $path));
|
||||
}
|
||||
|
||||
/**
|
||||
* Baut den URL-Pfad für eine Page rekursiv auf (Fallback wenn real_url_path nicht gesetzt)
|
||||
*
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue