b2in/app/Livewire/Admin/Cms/DisplayVersionEditor.php

700 lines
23 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\Admin\Cms;
use App\Enums\DisplayVersionType;
use App\Models\DisplayMedia;
use App\Models\DisplayVersion;
use App\Models\DisplayVersionItem;
use App\Services\DisplayQrCodeService;
use App\Support\DisplayModuleSettings;
use Livewire\Attributes\On;
use Livewire\Component;
class DisplayVersionEditor extends Component
{
public DisplayVersion $version;
public string $versionName = '';
// Item Modal
public bool $showItemModal = false;
public ?int $itemId = null;
public string $itemType = '';
// Video-Display: Video fields
public string $videoFilename = '';
public string $videoTitle = '';
public int $videoPosition = 25;
public bool $videoIsActive = true;
// Video-Display: Footer fields
public string $footerHeadline = '';
public string $footerSubline = '';
public string $footerUrl = '';
public bool $footerIsActive = true;
// B2in: Media fields
public string $mediaType = 'image';
public string $mediaCategory = 'immobilien';
public string $mediaUrl = '';
public string $mediaHeadline = '';
public string $mediaSubline = '';
public int $mediaDuration = 10;
public bool $mediaIsActive = true;
// Offers: Slide fields (single dynamic detail layout)
public bool $slideShowLogo = true;
public string $slideLogoUrl = '';
public string $slideBrandText = '';
public int $slideDuration = 8000;
public string $slideImageUrl = '';
public string $slideBadge = '';
public bool $slideShowBadge = true;
public string $slideEyebrow = '';
public bool $slideShowEyebrow = true;
public string $slideTitle = '';
public string $slideSubline = '';
public bool $slideShowSubline = false;
public string $slidePrice = '';
public string $slideOriginalPrice = '';
public bool $slideStrikeOriginalPrice = false;
public string $slideTagText = '';
public bool $slideShowPrice = false;
/** @var array<string> */
public array $slideBullets = [];
public bool $slideShowBullets = true;
public string $slideDisclaimer = '';
public bool $slideShowDisclaimer = false;
public string $slideQrUrl = '';
public string $slideQrTitle = '';
public bool $slideShowQr = true;
public string $slideContact = '';
public bool $slideShowContact = true;
public string $slideBrandTagline = '';
public bool $slideIsActive = true;
// Settings Modal
public bool $showSettingsModal = false;
public array $settings = [];
public int $previewFrameRefreshCounter = 0;
/** @var array<string> */
public const BRAND_POSITIONS = ['top-left', 'top-right', 'bottom-left', 'bottom-right'];
public function mount(DisplayVersion $displayVersion): void
{
$this->version = $displayVersion;
$this->versionName = $displayVersion->name;
$this->settings = $this->settingsWithDefaults();
$this->normalizeBrandPositions();
}
public function updated(string $name): void
{
if (str_starts_with($name, 'settings.show_footer')
|| str_starts_with($name, 'settings.logo_position')
|| str_starts_with($name, 'settings.claim_position')) {
$this->normalizeBrandPositions();
}
}
public function toggleTheme(): void
{
$settings = $this->settingsWithDefaults();
$settings['theme'] = ($settings['theme'] ?? 'dark') === 'dark' ? 'light' : 'dark';
$this->version->update(['settings' => $settings]);
$this->settings = $settings;
$this->refreshModulePreview();
}
public function saveName(): void
{
$this->validate([
'versionName' => 'required|string|max:255',
]);
$this->version->update(['name' => $this->versionName]);
$this->refreshModulePreview();
session()->flash('success', 'Name aktualisiert!');
}
// ========================================
// SETTINGS
// ========================================
public function openSettingsModal(): void
{
$this->settings = $this->settingsWithDefaults();
$this->showSettingsModal = true;
}
public function saveSettings(): void
{
$this->normalizeBrandPositions();
$this->settings = $this->settingsWithQrImage($this->settings);
$this->version->update(['settings' => $this->settings]);
$this->showSettingsModal = false;
$this->refreshModulePreview();
session()->flash('success', 'Einstellungen gespeichert!');
}
/**
* Keep the B2in logo/claim corners consistent:
* - bottom corners are only valid while the footer is hidden,
* - the claim can never sit in the same corner as the logo.
*/
private function normalizeBrandPositions(): void
{
if ($this->version->type !== DisplayVersionType::B2in) {
return;
}
$footerShown = ($this->settings['show_footer'] ?? true) !== false;
$allowed = $footerShown ? ['top-left', 'top-right'] : self::BRAND_POSITIONS;
$logo = $this->settings['logo_position'] ?? 'top-left';
$claim = $this->settings['claim_position'] ?? 'top-right';
$logo = $this->moveIntoAllowed($logo, $allowed);
$claim = $this->moveIntoAllowed($claim, $allowed);
if ($claim === $logo) {
$claim = collect($allowed)->first(fn (string $position) => $position !== $logo) ?? $claim;
}
$this->settings['logo_position'] = $logo;
$this->settings['claim_position'] = $claim;
}
/**
* @param array<string> $allowed
*/
private function moveIntoAllowed(string $position, array $allowed): string
{
if (in_array($position, $allowed, true)) {
return $position;
}
// Pull bottom corners up to the matching top corner when forbidden.
$fallback = str_replace('bottom-', 'top-', $position);
return in_array($fallback, $allowed, true) ? $fallback : ($allowed[0] ?? 'top-left');
}
// ========================================
// ITEM CRUD
// ========================================
public function openItemModal(?int $id = null, string $type = ''): void
{
$this->resetItemForm();
if ($id) {
$item = DisplayVersionItem::findOrFail($id);
$this->itemId = $item->id;
$this->itemType = $item->item_type;
$this->loadItemContent($item);
} else {
$this->itemType = $type ?: $this->defaultItemType();
}
$this->showItemModal = true;
}
public function saveItem(): void
{
$content = $this->buildItemContent();
$isActive = $this->getActiveFlag();
if ($this->itemId) {
$item = DisplayVersionItem::findOrFail($this->itemId);
$item->update([
'content' => $content,
'is_active' => $isActive,
]);
session()->flash('success', 'Inhalt aktualisiert!');
} else {
$maxSort = DisplayVersionItem::where('display_version_id', $this->version->id)
->where('item_type', $this->itemType)
->max('sort_order') ?? -1;
$item = DisplayVersionItem::create([
'display_version_id' => $this->version->id,
'item_type' => $this->itemType,
'content' => $content,
'sort_order' => $maxSort + 1,
'is_active' => $isActive,
]);
session()->flash('success', 'Inhalt hinzugefügt!');
}
$this->itemId = $item->id;
$this->itemType = $item->item_type;
$this->loadItemContent($item->fresh());
$this->showItemModal = true;
$this->refreshModulePreview();
}
public function deleteItem(int $id): void
{
DisplayVersionItem::findOrFail($id)->delete();
$this->refreshModulePreview();
session()->flash('success', 'Inhalt gelöscht!');
}
public function toggleItemStatus(int $id): void
{
$item = DisplayVersionItem::findOrFail($id);
$item->update(['is_active' => ! $item->is_active]);
$this->refreshModulePreview();
}
public function moveItem(int $id, string $direction): void
{
$item = DisplayVersionItem::findOrFail($id);
$currentOrder = $item->sort_order;
$swapItem = DisplayVersionItem::where('display_version_id', $this->version->id)
->where('item_type', $item->item_type)
->where('sort_order', $direction === 'up' ? $currentOrder - 1 : $currentOrder + 1)
->first();
if ($swapItem) {
$item->update(['sort_order' => $swapItem->sort_order]);
$swapItem->update(['sort_order' => $currentOrder]);
$this->refreshModulePreview();
}
}
public function modulePreviewUrl(): string
{
return url('/preview/module/'.$this->version->id).'?refresh='.$this->previewFrameRefreshCounter;
}
public function itemPreviewUrl(): string
{
if (! $this->itemId) {
return $this->modulePreviewUrl();
}
return url('/preview/module/'.$this->version->id.'/item/'.$this->itemId).'?refresh='.$this->previewFrameRefreshCounter;
}
#[On('display-media-selected')]
public function onDisplayMediaSelected(string $field, ?int $mediaId, ?string $url): void
{
if (! $url) {
return;
}
match ($field) {
'videoFilename' => $this->videoFilename = $url,
'mediaUrl' => $this->mediaUrl = $url,
'slideImageUrl' => $this->slideImageUrl = $url,
'slideLogoUrl' => $this->slideLogoUrl = $url,
'settings.header_logo_url' => $this->settings['header_logo_url'] = $url,
default => null,
};
// The media type for a B2in playlist item is derived from the chosen
// media – the type selector in the form is only an informational hint.
if ($field === 'mediaUrl' && $mediaId) {
$media = DisplayMedia::find($mediaId);
if ($media) {
$this->mediaType = $media->isVideo() ? 'video' : 'image';
}
}
}
public function addBullet(): void
{
$this->slideBullets[] = '';
}
public function removeBullet(int $index): void
{
unset($this->slideBullets[$index]);
$this->slideBullets = array_values($this->slideBullets);
}
public function closeItemModal(): void
{
$this->showItemModal = false;
$this->resetItemForm();
}
// ========================================
// HELPERS
// ========================================
private function loadItemContent(DisplayVersionItem $item): void
{
$content = $item->content;
$isActive = (bool) $item->is_active;
match ($item->item_type) {
'video' => $this->loadVideoContent($content, $isActive),
'footer' => $this->loadFooterContent($content, $isActive),
'media' => $this->loadMediaContent($content, $isActive),
'slide' => $this->loadSlideContent($content, $isActive),
default => null,
};
}
/**
* @param array<string, mixed> $content
*/
private function loadVideoContent(array $content, bool $isActive): void
{
$this->videoFilename = $content['filename'] ?? '';
$this->videoTitle = $content['title'] ?? '';
$this->videoPosition = $content['position'] ?? 25;
$this->videoIsActive = $isActive;
}
/**
* @param array<string, mixed> $content
*/
private function loadFooterContent(array $content, bool $isActive): void
{
$this->footerHeadline = $content['headline'] ?? '';
$this->footerSubline = $content['subline'] ?? '';
$this->footerUrl = $content['url'] ?? '';
$this->footerIsActive = $isActive;
}
/**
* @param array<string, mixed> $content
*/
private function loadMediaContent(array $content, bool $isActive): void
{
$this->mediaType = $content['media_type'] ?? 'image';
$this->mediaCategory = $content['category'] ?? 'immobilien';
$this->mediaUrl = $content['media_url'] ?? '';
$this->mediaHeadline = $content['headline'] ?? '';
$this->mediaSubline = $content['subline'] ?? '';
$this->mediaDuration = $content['duration_seconds'] ?? 10;
$this->mediaIsActive = $isActive;
}
/**
* @param array<string, mixed> $content
*/
private function loadSlideContent(array $content, bool $isActive): void
{
$this->slideShowLogo = $content['show_logo'] ?? true;
$this->slideLogoUrl = $content['logo_url'] ?? '';
$this->slideBrandText = $content['brand_text'] ?? '';
$this->slideDuration = $content['duration'] ?? 8000;
$this->slideImageUrl = $content['image_url'] ?? '';
$this->slideBadge = $content['badge_text'] ?? '';
$this->slideEyebrow = $content['eyebrow'] ?? '';
$this->slideTitle = $content['title'] ?? '';
$this->slideSubline = $content['subline'] ?? '';
$this->slidePrice = $content['price'] ?? '';
$this->slideOriginalPrice = $content['original_price'] ?? '';
$this->slideStrikeOriginalPrice = $content['strike_original_price'] ?? false;
$this->slideTagText = $content['tag_text'] ?? '';
$this->slideBullets = $content['bullets'] ?? [];
$this->slideDisclaimer = $content['disclaimer'] ?? '';
$this->slideQrUrl = $content['qr_url'] ?? '';
$this->slideQrTitle = $content['qr_title'] ?? '';
$this->slideContact = $content['contact'] ?? '';
$this->slideBrandTagline = $content['brand_tagline'] ?? '';
$this->slideIsActive = $isActive;
// Show flags fall back to "is there content?" so slides created before
// the dynamic detail layout keep rendering exactly as they did.
$this->slideShowBadge = $content['show_badge'] ?? ($content['badge_text'] ?? '') !== '';
$this->slideShowEyebrow = $content['show_eyebrow'] ?? ($content['eyebrow'] ?? '') !== '';
$this->slideShowSubline = $content['show_subline'] ?? ($content['subline'] ?? '') !== '';
$this->slideShowBullets = $content['show_bullets'] ?? ! empty($content['bullets']);
$this->slideShowPrice = $content['show_price'] ?? ($content['price'] ?? '') !== '';
$this->slideShowDisclaimer = $content['show_disclaimer'] ?? ($content['disclaimer'] ?? '') !== '';
$this->slideShowQr = $content['show_qr'] ?? ($content['qr_url'] ?? '') !== '';
$this->slideShowContact = $content['show_contact'] ?? ($content['contact'] ?? '') !== '';
}
/**
* @return array<string, mixed>
*/
private function buildItemContent(): array
{
$content = match ($this->itemType) {
'video' => [
'filename' => $this->videoFilename,
'title' => $this->videoTitle,
'position' => $this->videoPosition,
],
'footer' => [
'headline' => $this->footerHeadline,
'subline' => $this->footerSubline,
'url' => $this->footerUrl ?: null,
],
'media' => [
'media_type' => $this->mediaType,
'category' => $this->mediaCategory,
'media_url' => $this->mediaUrl,
'headline' => $this->mediaHeadline,
'subline' => $this->mediaSubline,
'duration_seconds' => $this->mediaDuration,
],
'slide' => [
'type' => 'detail',
'show_logo' => $this->slideShowLogo,
'logo_url' => $this->slideLogoUrl,
'brand_text' => $this->slideBrandText,
'duration' => $this->slideDuration,
'image_url' => $this->slideImageUrl,
'badge_text' => $this->slideBadge,
'show_badge' => $this->slideShowBadge,
'eyebrow' => $this->slideEyebrow,
'show_eyebrow' => $this->slideShowEyebrow,
'title' => $this->slideTitle,
'subline' => $this->slideSubline,
'show_subline' => $this->slideShowSubline,
'price' => $this->slidePrice,
'original_price' => $this->slideOriginalPrice,
'strike_original_price' => $this->slideStrikeOriginalPrice,
'tag_text' => $this->slideTagText,
'show_price' => $this->slideShowPrice,
'bullets' => array_values(array_filter($this->slideBullets, fn (string $bullet) => trim($bullet) !== '')),
'show_bullets' => $this->slideShowBullets,
'disclaimer' => $this->slideDisclaimer,
'show_disclaimer' => $this->slideShowDisclaimer,
'qr_url' => $this->slideQrUrl,
'qr_title' => $this->slideQrTitle,
'show_qr' => $this->slideShowQr,
'contact' => $this->slideContact,
'show_contact' => $this->slideShowContact,
'brand_tagline' => $this->slideBrandTagline,
],
default => [],
};
return $this->contentWithQrImage($content);
}
/**
* @param array<string, mixed> $content
* @return array<string, mixed>
*/
private function contentWithQrImage(array $content): array
{
$qrUrl = match ($this->itemType) {
'footer' => $content['url'] ?? null,
'slide' => $content['qr_url'] ?? null,
default => null,
};
$qrCodeService = app(DisplayQrCodeService::class);
$qrCode = $qrCodeService->generateForUrl(is_string($qrUrl) ? $qrUrl : null);
if ($qrCode) {
$content['qr_image_path'] = $qrCode['path'];
$content['qr_image_url'] = $qrCode['url'];
} elseif ($this->itemId && is_string($qrUrl) && $qrUrl !== '') {
$existingContent = DisplayVersionItem::find($this->itemId)?->content ?? [];
$existingQrUrl = match ($this->itemType) {
'footer' => $existingContent['url'] ?? null,
'slide' => $existingContent['qr_url'] ?? null,
default => null,
};
$preservedExistingQrImage = false;
if (is_string($existingQrUrl)
&& $qrCodeService->normalizeUrl($existingQrUrl) === $qrCodeService->normalizeUrl($qrUrl)
&& ! empty($existingContent['qr_image_path'])
&& ! empty($existingContent['qr_image_url'])) {
$content['qr_image_path'] = $existingContent['qr_image_path'];
$content['qr_image_url'] = $existingContent['qr_image_url'];
$preservedExistingQrImage = true;
}
if (! $preservedExistingQrImage) {
unset($content['qr_image_path'], $content['qr_image_url']);
}
} else {
unset($content['qr_image_path'], $content['qr_image_url']);
}
return $content;
}
/**
* @param array<string, mixed> $settings
* @return array<string, mixed>
*/
private function settingsWithQrImage(array $settings): array
{
if ($this->version->type !== DisplayVersionType::B2in) {
return $settings;
}
$qrUrl = $settings['qr_url'] ?? $settings['footer_url'] ?? null;
$qrCodeService = app(DisplayQrCodeService::class);
$qrCode = $qrCodeService->generateForUrl(is_string($qrUrl) ? $qrUrl : null);
if ($qrCode) {
$settings['qr_image_path'] = $qrCode['path'];
$settings['qr_image_url'] = $qrCode['url'];
} elseif (is_string($qrUrl) && $qrUrl !== '') {
$existingSettings = $this->settingsWithDefaults();
$existingQrUrl = $existingSettings['qr_url'] ?? $existingSettings['footer_url'] ?? null;
$preservedExistingQrImage = false;
if (is_string($existingQrUrl)
&& $qrCodeService->normalizeUrl($existingQrUrl) === $qrCodeService->normalizeUrl($qrUrl)
&& ! empty($existingSettings['qr_image_path'])
&& ! empty($existingSettings['qr_image_url'])) {
$settings['qr_image_path'] = $existingSettings['qr_image_path'];
$settings['qr_image_url'] = $existingSettings['qr_image_url'];
$preservedExistingQrImage = true;
}
if (! $preservedExistingQrImage) {
unset($settings['qr_image_path'], $settings['qr_image_url']);
}
} else {
unset($settings['qr_image_path'], $settings['qr_image_url']);
}
return $settings;
}
private function getActiveFlag(): bool
{
return match ($this->itemType) {
'video' => $this->videoIsActive,
'footer' => $this->footerIsActive,
'media' => $this->mediaIsActive,
'slide' => $this->slideIsActive,
default => true,
};
}
private function defaultItemType(): string
{
return match ($this->version->type) {
DisplayVersionType::VideoDisplay => 'video',
DisplayVersionType::B2in => 'media',
DisplayVersionType::Offers => 'slide',
};
}
private function resetItemForm(): void
{
$this->itemId = null;
$this->itemType = '';
$this->videoFilename = '';
$this->videoTitle = '';
$this->videoPosition = 25;
$this->videoIsActive = true;
$this->footerHeadline = '';
$this->footerSubline = '';
$this->footerUrl = '';
$this->footerIsActive = true;
$this->mediaType = 'image';
$this->mediaCategory = 'immobilien';
$this->mediaUrl = '';
$this->mediaHeadline = '';
$this->mediaSubline = '';
$this->mediaDuration = 10;
$this->mediaIsActive = true;
$this->slideShowLogo = true;
$this->slideLogoUrl = '';
$this->slideBrandText = '';
$this->slideDuration = 8000;
$this->slideImageUrl = '';
$this->slideBadge = '';
$this->slideShowBadge = true;
$this->slideEyebrow = '';
$this->slideShowEyebrow = true;
$this->slideTitle = '';
$this->slideSubline = '';
$this->slideShowSubline = false;
$this->slidePrice = '';
$this->slideOriginalPrice = '';
$this->slideStrikeOriginalPrice = false;
$this->slideTagText = '';
$this->slideShowPrice = false;
$this->slideBullets = [];
$this->slideShowBullets = true;
$this->slideDisclaimer = '';
$this->slideShowDisclaimer = false;
$this->slideQrUrl = '';
$this->slideQrTitle = '';
$this->slideShowQr = true;
$this->slideContact = '';
$this->slideShowContact = true;
$this->slideBrandTagline = '';
$this->slideIsActive = true;
}
private function refreshModulePreview(): void
{
$this->previewFrameRefreshCounter++;
}
/**
* @return array<string, mixed>
*/
private function settingsWithDefaults(): array
{
return DisplayModuleSettings::merge($this->version->type, $this->version->settings);
}
public function render()
{
$items = $this->version->items()->get()->groupBy('item_type');
return view('livewire.admin.cms.display-version-editor', [
'items' => $items,
]);
}
}