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,62 @@
<?php
namespace Database\Factories;
use App\Models\CmsReference;
use Illuminate\Database\Eloquent\Factories\Factory;
/**
* @extends Factory<CmsReference>
*/
class CmsReferenceFactory extends Factory
{
protected $model = CmsReference::class;
public function definition(): array
{
$title = fake()->unique()->words(3, true);
return [
'title' => ['de' => ucfirst($title), 'en' => ucfirst($title)],
'location' => fake()->city(),
'year' => (string) fake()->numberBetween(2022, 2026),
'description' => ['de' => fake()->paragraph(2), 'en' => fake()->paragraph(2)],
'image' => 'referenzen/'.fake()->unique()->slug(2).'.webp',
'gallery' => [],
'brand_slugs' => [],
'is_published' => true,
'order' => fake()->numberBetween(1, 20),
];
}
public function published(): static
{
return $this->state(fn () => ['is_published' => true]);
}
public function unpublished(): static
{
return $this->state(fn () => ['is_published' => false]);
}
/**
* @param list<string> $slugs
*/
public function forBrands(array $slugs): static
{
return $this->state(fn () => ['brand_slugs' => $slugs]);
}
public function withGallery(int $count = 2): static
{
return $this->state(function () use ($count) {
$items = [];
for ($i = 1; $i <= $count; $i++) {
$items[] = ['image' => 'referenzen/bild-'.$i.'.webp', 'caption' => 'Bild '.$i];
}
return ['gallery' => $items];
});
}
}

View file

@ -0,0 +1,37 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
/**
* Fertigungsort je Kernmarke (Nachtrag 11 zu Arbeitsplan 14.09.2026, Punkt 1.3).
*
* Die Herkunfts-Sektion „Aus der Region gefertigt“ holt ihre Standorte nicht mehr aus einem
* JSON-Eintrag im generischen Inhalte-Editor, sondern aus der Markenverwaltung: Marcel pflegt
* Ort, Entfernung und Rolle direkt bei der Marke. Die Kartenpositionen bleiben im Code.
*
* - `origin_city` Fertigungsort, z. B. „Bünde“ – leer bedeutet: Marke erscheint nicht in der Sektion
* - `origin_distance` Entfernung ab Bielefeld als Text, z. B. „rund 30 km“
* - `origin_role` Ein Satz zur Rolle des Standorts
*
* Alle drei sind übersetzbar wie die übrigen Textfelder der Marke.
*/
return new class extends Migration
{
public function up(): void
{
Schema::table('cms_brands', function (Blueprint $table) {
$table->json('origin_city')->nullable()->after('showroom');
$table->json('origin_distance')->nullable()->after('origin_city');
$table->json('origin_role')->nullable()->after('origin_distance');
});
}
public function down(): void
{
Schema::table('cms_brands', function (Blueprint $table) {
$table->dropColumn(['origin_city', 'origin_distance', 'origin_role']);
});
}
};

View file

@ -0,0 +1,44 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
/**
* Referenzprojekte der Interior-Welt (Nachtrag 13).
*
* AP5 hatte die Referenzen aus den Markengalerien zusammengesammelt, um keine zweite Pflegestelle
* zu schaffen. Abstimmung 15.09.: Marcel soll Referenzen als eigene Einträge pflegen – ein
* realisiertes Projekt hat mehr als ein Bild und mehr als eine Marke. Solange kein Eintrag
* existiert, greift das Frontend weiter auf die Markengalerien zurück.
*
* - `title`, `description` übersetzbar
* - `location`, `year` Ort und Jahr als Text („Bielefeld“, „2025“)
* - `image` Titelbild (Dateiname aus der Medienbibliothek)
* - `gallery` weitere Bilder mit Bildunterschrift
* - `brand_slugs` beteiligte Kernmarken (Slugs aus cms_brands), verlinkt auf die Markenseiten
*/
return new class extends Migration
{
public function up(): void
{
Schema::create('cms_references', function (Blueprint $table) {
$table->id();
$table->json('title');
$table->string('location')->nullable();
$table->string('year', 20)->nullable();
$table->json('description')->nullable();
$table->string('image')->nullable();
$table->json('gallery')->nullable();
$table->json('brand_slugs')->nullable();
$table->boolean('is_published')->default(true);
$table->integer('order')->default(0);
$table->timestamps();
});
}
public function down(): void
{
Schema::dropIfExists('cms_references');
}
};

View file

@ -0,0 +1,87 @@
<?php
namespace Database\Seeders;
use App\Models\CmsBrand;
use Illuminate\Database\Seeder;
/**
* Trägt den Fertigungsort der Kernmarken nach (Nachtrag 11 zu Arbeitsplan 14.09.2026, Punkt 1.3).
*
* Die Herkunfts-Sektion „Aus der Region gefertigt“ liest Ort, Entfernung und Rolle aus der
* Marke. {@see CmsBrandSeeder} bringt dieselben Werte für frische Installationen mit,
* überspringt aber bereits angelegte Marken. Dieser Seeder fasst nur die drei Felder an und
* auch nur bei Marken, deren Fertigungsort noch leer ist – ein im Admin eingetragener Ort
* bleibt stehen. Mehrfaches Ausführen ist unbedenklich.
*
* Entfernungen gerundet, Straße ab Bielefeld Mitte. ⚠️ Rolle CABINET nach Kevins Verständnis
* (Fertigung Köln, Beratung im Store Bielefeld) – von Marcel bestätigen lassen.
*
* php artisan db:seed --class=BrandOriginSeeder
*/
class BrandOriginSeeder extends Seeder
{
/** @var array<string, array{origin_city: array<string, string>, origin_distance: array<string, string>, origin_role: array<string, string>}> */
private const ORIGINS = [
'cabinet' => [
'origin_city' => ['de' => 'Köln', 'en' => 'Cologne'],
'origin_distance' => ['de' => 'rund 200 km', 'en' => 'about 200 km'],
'origin_role' => [
'de' => 'Gefertigt in der Kölner Manufaktur, beraten und geplant im CABINET Store Bielefeld.',
'en' => 'Made in the Cologne manufactory, advised and planned at the CABINET Store Bielefeld.',
],
],
'rotpunkt' => [
'origin_city' => ['de' => 'Bünde', 'en' => 'Bünde'],
'origin_distance' => ['de' => 'rund 30 km', 'en' => 'about 30 km'],
'origin_role' => [
'de' => 'Küchen aus eigener Fertigung in Bünde – geplant bei uns im Küchenstudio.',
'en' => 'Kitchens from their own production in Bünde – planned with us in the kitchen studio.',
],
],
'februe' => [
'origin_city' => ['de' => 'Herford', 'en' => 'Herford'],
'origin_distance' => ['de' => 'rund 20 km', 'en' => 'about 20 km'],
'origin_role' => [
'de' => 'Büro- und Objektmöbel aus eigener Fertigung in Herford.',
'en' => 'Office and contract furniture from their own production in Herford.',
],
],
'tischlerei' => [
'origin_city' => ['de' => 'Rheda-Wiedenbrück', 'en' => 'Rheda-Wiedenbrück'],
'origin_distance' => ['de' => 'rund 35 km', 'en' => 'about 35 km'],
'origin_role' => [
'de' => 'Unsere Werkstatt für Aufbau, Sonderlösungen und Veredelung.',
'en' => 'Our workshop for assembly, custom solutions and finishing.',
],
],
];
public function run(): void
{
$set = 0;
$kept = 0;
foreach (self::ORIGINS as $slug => $origin) {
$brand = CmsBrand::query()->where('slug', $slug)->first();
if (! $brand) {
$this->command?->warn("Marke „{$slug}“ nicht gefunden – übersprungen.");
continue;
}
if (trim((string) $brand->getTranslation('origin_city', 'de', false)) !== '') {
$kept++;
continue;
}
$brand->fill($origin);
$brand->save();
$set++;
}
$this->command?->info("Fertigungsorte gesetzt: {$set}, vorhandene behalten: {$kept}.");
}
}

View file

@ -15,7 +15,8 @@ use Illuminate\Database\Seeder;
* Partnerbetriebs gehört laut Plan auf die Detailseite, nicht in die Marke.
*
* Bestehende Marken werden nicht überschrieben – Marcel pflegt sie im Admin. Für bereits
* angelegte Marken setzt {@see BrandLogoSeeder} die Logos nach.
* angelegte Marken setzt {@see BrandLogoSeeder} die Logos nach und {@see BrandOriginSeeder}
* den Fertigungsort für die Herkunfts-Sektion (Nachtrag 11).
*
* Stimmungsbilder und Galerien fehlen als Zulieferung und bleiben leer; die Kacheln und
* Detailseiten zeigen so lange einen Platzhalter.
@ -77,6 +78,12 @@ Experience the possibilities in our Bielefeld showroom.',
['title' => 'Montage durch Fachleute', 'description' => 'Wir liefern und montieren – bis zum letzten Detail.'],
],
'showroom' => 'Showroom Bielefeld',
'origin_city' => ['de' => 'Köln', 'en' => 'Cologne'],
'origin_distance' => ['de' => 'rund 200 km', 'en' => 'about 200 km'],
'origin_role' => [
'de' => 'Gefertigt in der Kölner Manufaktur, beraten und geplant im CABINET Store Bielefeld.',
'en' => 'Made in the Cologne manufactory, advised and planned at the CABINET Store Bielefeld.',
],
'is_published' => true,
],
[
@ -112,6 +119,12 @@ Consultation and planning in our kitchen studio.',
['title' => 'Lieferung und Aufbau', 'description' => 'Koordiniert aus einer Hand, inklusive Anschluss.'],
],
'showroom' => 'Küchenstudio',
'origin_city' => ['de' => 'Bünde', 'en' => 'Bünde'],
'origin_distance' => ['de' => 'rund 30 km', 'en' => 'about 30 km'],
'origin_role' => [
'de' => 'Küchen aus eigener Fertigung in Bünde – geplant bei uns im Küchenstudio.',
'en' => 'Kitchens from their own production in Bünde – planned with us in the kitchen studio.',
],
'is_published' => true,
],
[
@ -143,6 +156,12 @@ Consultation and planning in our kitchen studio.',
['title' => 'Akustik und Ordnung', 'description' => 'Stauraum und Raumgliederung, die Konzentration ermöglichen.'],
],
'showroom' => '',
'origin_city' => ['de' => 'Herford', 'en' => 'Herford'],
'origin_distance' => ['de' => 'rund 20 km', 'en' => 'about 20 km'],
'origin_role' => [
'de' => 'Büro- und Objektmöbel aus eigener Fertigung in Herford.',
'en' => 'Office and contract furniture from their own production in Herford.',
],
'is_published' => true,
],
[
@ -223,6 +242,12 @@ It complements CABINET and Rotpunkt wherever making to drawing is required: asse
],
],
'showroom' => '',
'origin_city' => ['de' => 'Rheda-Wiedenbrück', 'en' => 'Rheda-Wiedenbrück'],
'origin_distance' => ['de' => 'rund 35 km', 'en' => 'about 35 km'],
'origin_role' => [
'de' => 'Unsere Werkstatt für Aufbau, Sonderlösungen und Veredelung.',
'en' => 'Our workshop for assembly, custom solutions and finishing.',
],
'is_published' => true,
],
];

View file

@ -2,13 +2,21 @@
namespace Database\Seeders;
use FluxCms\Core\Models\CmsContent;
use FluxCms\Core\Services\CmsContentService;
use Illuminate\Database\Seeder;
/**
* Herkunfts-Sektion „Aus der Region gefertigt“ ins CMS legen (Arbeitsplan 14.09.2026, Punkt 1.3).
*
* Legt die Sektion nur an, wenn sie fehlt. Ein bestehender Eintrag bleibt unangetastet,
* damit Korrekturen aus dem Admin (Entfernungen, Ablauf CABINET) nicht verloren gehen.
* Legt die Sektion nur an, wenn sie fehlt. Ein bestehender Eintrag bleibt inhaltlich unangetastet,
* damit Korrekturen aus dem Admin (Ablauf CABINET) nicht verloren gehen.
*
* Einzige Ausnahme (Nachtrag 11): Der Schlüssel `locations` stammt aus dem ersten Stand, als die
* Standorte noch im Sektions-JSON lagen. Seitdem kommen sie aus den Marken
* ({@see BrandOriginSeeder}), das Frontend liest den Schlüssel nicht mehr. Er wird aus einem
* bestehenden Eintrag entfernt, damit er im Inhalte-Editor nicht als Pflegefeld auftaucht.
* Mehrfaches Ausführen ist unbedenklich.
*
* php artisan db:seed --class=OriginSectionCmsSeeder
*/
@ -24,6 +32,38 @@ class OriginSectionCmsSeeder extends Seeder
$seeder->onlySections = self::SECTIONS;
$seeder->run();
$this->dropLegacyLocations();
$this->command?->info('Herkunfts-Sektion: angelegt, falls sie fehlte.');
}
private function dropLegacyLocations(): void
{
$row = CmsContent::query()->where('group', 'home')->where('key', 'origin_section')->first();
if (! $row) {
return;
}
$value = $row->getTranslations('value');
$changed = false;
foreach ($value as $locale => $section) {
if (is_array($section) && array_key_exists('locations', $section)) {
unset($section['locations']);
$value[$locale] = $section;
$changed = true;
}
}
if (! $changed) {
return;
}
$row->value = $value;
$row->save();
app(CmsContentService::class)->clearCache('home');
$this->command?->info('Herkunfts-Sektion: veralteten Schlüssel „locations“ entfernt – die Standorte kommen aus den Marken.');
}
}