CMS: Standorte an der Marke, Seitenbaum mit Vorschau, Referenzen-Modul

Nachtrag 11: Herkunfts-Sektion liest Ort, Entfernung und Rolle aus der
Markenverwaltung (drei neue Felder an cms_brands, BrandOriginSeeder),
Kartenpositionen im Code, „Marken“ in der Sidebar.

Nachtrag 12: Inhalte-Editor zeigt die Seiten der Website mit ihren
Sektionen in Reihenfolge (config/cms_pages.php, CmsPageTree), Felder in
Klartext, fehlende Sektionen aus der Sprachdatei anlegbar, Vorschau der
echten Seite im iframe mit Sprung zur Sektion; Anker an allen lebenden
Sektionen nachgezogen.

Nachtrag 13: Referenzen als eigenes Modul (CmsReference, Portal-Seite,
/referenzen mit Fallback auf die Markengalerien); Immobilien-Projekte
aus Sidebar und CMS-Übersicht genommen, Route bleibt.

Dazu .gitignore für _b2in_interior_shop/ und docs/.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
Kevin Adametz 2026-09-15 14:58:45 +00:00
parent 7953a8c9a1
commit 90aa079cf7
50 changed files with 2580 additions and 324 deletions

View file

@ -0,0 +1,187 @@
<?php
namespace App\Services;
use FluxCms\Core\Models\CmsContent;
use Illuminate\Support\Str;
/**
* Seitenbaum für den Inhalte-Editor (config/cms_pages.php, Nachtrag 12).
*
* Verbindet die Registry mit dem Datenbestand: je Sektion, ob sie in flux_cms_contents liegt
* (dann mit `content_id`), und was sonst noch in der Datenbank steht, aber in keiner Seite
* eingetragen ist („Nicht mehr verwendet“).
*/
class CmsPageTree
{
/** Schlüssel des Sammelpunkts für nicht eingetragene Sektionen. */
public const UNLISTED = 'unlisted';
/**
* Alle Seiten mit ihren Sektionen in Website-Reihenfolge.
*
* @return array<string, array{title: string, path: ?string, sections: list<array<string, mixed>>}>
*/
public function pages(): array
{
$rows = CmsContent::query()
->get(['id', 'group', 'key'])
->keyBy(fn (CmsContent $row) => $row->group.'/'.$row->key);
$pages = [];
foreach (config('cms_pages.pages', []) as $pageKey => $page) {
$sections = [];
foreach ($page['sections'] ?? [] as $section) {
$id = $section['group'].'/'.$section['key'];
$sections[] = array_merge($section, [
'page' => $pageKey,
'path' => $section['path'] ?? $page['path'] ?? null,
'anchor' => $section['anchor'] ?? null,
'content_id' => $rows[$id]->id ?? null,
'theme_key' => $this->themeKey($section['group'], $section['key']),
]);
}
$pages[$pageKey] = [
'title' => $page['title'],
'path' => $page['path'] ?? null,
'sections' => $sections,
];
}
$unlisted = $this->unlistedSections($rows->keys()->all(), $rows);
if ($unlisted !== []) {
$pages[self::UNLISTED] = [
'title' => config('cms_pages.unlisted.title', 'Nicht mehr verwendet'),
'path' => null,
'sections' => $unlisted,
];
}
return $pages;
}
/**
* Eine Sektion aus der Registry samt Datenbankbezug, oder null.
*
* @return array<string, mixed>|null
*/
public function section(string $group, string $key): ?array
{
foreach ($this->pages() as $page) {
foreach ($page['sections'] as $section) {
if ($section['group'] === $group && $section['key'] === $key) {
return $section;
}
}
}
return null;
}
/**
* Klartext-Beschriftung eines Feldes: Sektions-Override, dann Wörterbuch, sonst lesbar gemacht.
*/
public function fieldLabel(string $field, ?string $group = null, ?string $key = null): string
{
if ($group !== null && $key !== null) {
foreach (config('cms_pages.pages', []) as $page) {
foreach ($page['sections'] ?? [] as $section) {
if ($section['group'] === $group && $section['key'] === $key && isset($section['fields'][$field])) {
return $section['fields'][$field];
}
}
}
}
$labels = config('cms_pages.field_labels', []);
if (isset($labels[$field])) {
return $labels[$field];
}
return Str::ucfirst(str_replace('_', ' ', $field));
}
/**
* Adresse der Vorschau für eine Sektion: Frontend-Domain, Pfad, Cache-Brecher und Sprungmarke.
*/
public function previewUrl(?array $section, int $version = 0): ?string
{
$path = $section['path'] ?? null;
if ($path === null) {
return null;
}
$url = rtrim(config('domains.protocol', 'https://').config('domains.domain_b2in'), '/')
.'/'.ltrim($path, '/')
.'?cms-preview='.$version;
if (! empty($section['anchor'])) {
$url .= '#'.$section['anchor'];
}
return $url;
}
/**
* Theme-Section-Schlüssel (Lang-Datei) zu Gruppe/Key, für „Aus Vorlage anlegen“.
*/
public function themeKey(string $group, string $key): ?string
{
foreach (config('cms_section_map.sections', []) as $themeKey => $target) {
if ($target === [$group, $key]) {
return $themeKey;
}
}
return null;
}
/**
* @param list<string> $ids
* @param \Illuminate\Support\Collection<string, CmsContent> $rows
* @return list<array<string, mixed>>
*/
private function unlistedSections(array $ids, $rows): array
{
$listed = [];
foreach (config('cms_pages.pages', []) as $page) {
foreach ($page['sections'] ?? [] as $section) {
$listed[$section['group'].'/'.$section['key']] = true;
}
}
$out = [];
foreach ($ids as $id) {
if (isset($listed[$id])) {
continue;
}
$row = $rows[$id];
$out[] = [
'group' => $row->group,
'key' => $row->key,
'page' => self::UNLISTED,
'path' => null,
'anchor' => null,
'title' => $row->group.' / '.$row->key,
'description' => null,
'content_id' => $row->id,
'theme_key' => null,
];
}
usort($out, fn (array $a, array $b) => [$a['group'], $a['key']] <=> [$b['group'], $b['key']]);
return $out;
}
}