b2in/app/Livewire/Web/Components/UI/AppointmentWidget.php
Kevin Adametz 7953a8c9a1 Interior-Farbwelt, Welten-Trennung, Tischlerei und Herkunfts-Sektion
Arbeitsplan 14.09.2026, Punkte 1.1 bis 1.4 (Nachträge 6 bis 10 im Entwicklungsplan):

- Farbwelt Interior (Tanne, Creme, Sand, Ocker, Clay) als Standard, Immobilien-Welt
  behält das bisherige Blau über body.world-immobilien; Logo je Welt, Favicons neu,
  Welten-Wechsler in Kopf- und Fußzeile.
- Saubere Trennung der Welten: eigene Navigation, Ankündigungsleiste, Magazinliste,
  Über-uns-Seite, FAQ, Kontaktseite und Abschluss-CTA für Immobilien; Beiträge tragen
  eine Welt (Migration + Admin-Feld). Abschluss-CTA Ton in Ton statt Grün-zu-Ocker.
- Kontakt: Betreffe in Gruppen Interior / Immobilien / B2in, Marken darin.
  Terminanfrage-Buttons öffnen das Widget mit vorgewähltem Anliegen; Select-Felder
  einheitlich gestaltet.
- Tischlerei als vierte Kernmarke (Platzhalter), Kachelraster 2×2.
- Herkunfts-Sektion „Aus der Region gefertigt“ mit schematischer Standortkarte und
  Ablauf CABINET (Beratung Bielefeld, Fertigung Köln, Aufbau mit Tischlerei).

Deployment: migrate, WorldSplitCmsSeeder, CmsBrandSeeder, OriginSectionCmsSeeder
(Abschnitt 8 im Entwicklungsplan).

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-15 12:46:41 +00:00

166 lines
5.1 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.

<?php
namespace App\Livewire\Web\Components\Ui;
use Acme\ContactForm\ContactFormService;
use Acme\ContactForm\SpamDetector;
use App\Models\CmsBrand;
use Livewire\Component;
/**
* Schwebende Terminanfrage – auf allen Seiten unten rechts.
*
* Setzt wie die übrigen Formulare der Seite auf {@see ContactFormService} und {@see SpamDetector}
* auf, statt auf Acme\ContactForm\Livewire\ContactForm: dessen mitgelieferte View bringt ein
* eigenes (blaues) Layout mit, das nicht zum Auftritt passt. Honeypot, Rate-Limit und
* Spam-Erkennung kommen so trotzdem aus dem Paket.
*/
class AppointmentWidget extends Component
{
/** Feste Anliegen neben den gepflegten Marken. */
public const EXTRA_TOPICS = [
'projektausstattung' => 'Projektausstattung',
'immobilien' => 'Immobilien',
'sonstiges' => 'Sonstiges',
];
/** Farbwelt der Seite – in der Immobilien-Welt ist das Anliegen vorbelegt. */
public string $world = 'interior';
public string $name = '';
public string $email = '';
public string $phone = '';
public string $topic = '';
public string $preferredDate = '';
public string $message = '';
public bool $privacy = false;
public bool $success = false;
/** @var string Honeypot – muss leer bleiben. */
public string $website = '';
public ?int $formLoadedAt = null;
/** @var array<string, mixed> */
public array $content = [];
public function mount(): void
{
$section = cms_theme_section('appointment_widget');
$this->content = is_array($section) ? $section : [];
$this->formLoadedAt = time();
$this->world = b2in_world();
if ($this->world === 'immobilien') {
$this->topic = 'immobilien';
}
$this->applyBrandContext();
}
/**
* Auf einer Markenseite trägt der Button die Beschriftung dieser Marke.
*
* Die Zulieferung sieht ausdrücklich keinen zweiten CTA-Button vor, sondern eine
* kontextbezogene Beschriftung des vorhandenen Widgets. Da das Widget im Layout steckt und
* nicht je Seite eingebunden wird, liest es den Kontext aus der Route.
*/
private function applyBrandContext(): void
{
$route = request()->route();
if ($route?->getName() !== 'marken.show') {
return;
}
$slug = (string) $route->parameter('slug');
$brand = CmsBrand::forFrontend()[$slug] ?? null;
$label = is_array($brand) ? trim((string) ($brand['widget_label'] ?? '')) : '';
if ($label !== '') {
$this->content['button_label'] = $label;
}
}
/**
* Auswahl „Anliegen": gepflegte Kernmarken zuerst, danach die festen Einträge.
*
* @return array<string, string>
*/
public function getTopicsProperty(): array
{
$brands = [];
foreach (CmsBrand::forFrontend() as $slug => $brand) {
$brands[$slug] = (string) ($brand['name'] ?? $slug);
}
return $brands + self::EXTRA_TOPICS;
}
public function getWhatsappUrlProperty(): string
{
return whatsapp_link();
}
public function submit(ContactFormService $service): void
{
$this->validate([
'name' => ['required', 'string', 'max:255'],
'email' => ['required', 'email:rfc', 'max:255'],
'phone' => ['nullable', 'string', 'max:80'],
'topic' => ['nullable', 'string', 'max:255'],
'preferredDate' => ['nullable', 'date', 'after_or_equal:today'],
'message' => ['nullable', 'string', 'max:2000'],
'privacy' => ['accepted'],
'website' => ['nullable', 'string', 'max:0'],
], [
'preferredDate.after_or_equal' => __('ui.appointment.date_in_past'),
]);
$topicLabel = $this->topics[$this->topic] ?? '';
$payload = [
'name' => $this->name,
'email' => $this->email,
'phone' => $this->phone,
'topic' => $topicLabel,
'preferred_date' => $this->preferredDate,
'message' => $this->message,
'website' => $this->website,
'source' => request()->path(),
'ip' => request()->ip(),
'user_agent' => request()->userAgent(),
'is_spam' => false,
];
$payload['is_spam'] = SpamDetector::fromConfig()->detect($payload, $this->formLoadedAt);
$subject = ($this->world === 'immobilien' ? 'Terminanfrage B2in Immobilien' : 'Terminanfrage B2in Interior')
.($topicLabel !== '' ? ': '.$topicLabel : '');
$service->handle($payload, $subject, 'appointment-widget');
$this->success = true;
$this->reset(['name', 'email', 'phone', 'topic', 'preferredDate', 'message', 'privacy', 'website']);
$this->topic = $this->world === 'immobilien' ? 'immobilien' : '';
$this->formLoadedAt = time();
}
public function startOver(): void
{
$this->success = false;
$this->formLoadedAt = time();
}
public function render(): \Illuminate\View\View
{
return view('livewire.web.components.ui.appointment-widget');
}
}