Startseiten-Reisen im CMS steuerbar machen

This commit is contained in:
Kevin Adametz 2026-05-28 15:06:22 +00:00
parent ba48745809
commit 313f0dbf4e
5 changed files with 446 additions and 22 deletions

View file

@ -68,9 +68,24 @@ class CMSSidebarController extends Controller
$data['active'] = isset($data['active']) ? true : false; $data['active'] = isset($data['active']) ? true : false;
$data['show_at'] = isset($data['show_at']) ? $data['show_at'] : null; $data['show_at'] = isset($data['show_at']) ? $data['show_at'] : null;
$widget->fill($data)->save(); $component = isset($data['component']) ? $data['component'] : null;
$widget->save(); if (in_array($component, [SidebarWidget::HOMEPAGE_PLANNABLE_TRIPS, SidebarWidget::HOMEPAGE_POPULAR_TRIPS])) {
$pageIds = isset($data['homepage_page_ids']) ? array_values(array_filter($data['homepage_page_ids'])) : [];
$newPageIds = isset($data['homepage_new_page_ids']) ? array_values(array_filter($data['homepage_new_page_ids'])) : [];
$data['html'] = json_encode([
'page_ids' => $pageIds,
'new_page_ids' => array_values(array_intersect($pageIds, $newPageIds)),
'new_badge_active' => false,
]);
} elseif ($component === SidebarWidget::NEWS_SIDEBAR_WIDGET && isset($data['homepage_news_limit'])) {
$data['html'] = json_encode([
'news_limit' => max(1, min(12, (int) $data['homepage_news_limit'])),
]);
}
$widget->fill($data)->save();
\Session()->flash('alert-save', '1'); \Session()->flash('alert-save', '1');
return redirect(route('cms_sidebar_detail', [$widget->id])); return redirect(route('cms_sidebar_detail', [$widget->id]));

View file

@ -32,6 +32,10 @@ use Illuminate\Database\Eloquent\Model;
*/ */
class SidebarWidget extends Model class SidebarWidget extends Model
{ {
const HOMEPAGE_PLANNABLE_TRIPS = 'homepagePlannableTrips';
const HOMEPAGE_POPULAR_TRIPS = 'homepagePopularTrips';
const NEWS_SIDEBAR_WIDGET = 'newsSidebarWidget';
protected static $shows = [ protected static $shows = [
'home' => 'Startseite', 'home' => 'Startseite',
@ -54,7 +58,9 @@ class SidebarWidget extends Model
'travelGuideSidebarWidget' => 'Reiseführer', 'travelGuideSidebarWidget' => 'Reiseführer',
'travelMagazineSidebarWidget' => 'Reisemagazin', 'travelMagazineSidebarWidget' => 'Reisemagazin',
'offersSidebarWidget' => 'Angebote', 'offersSidebarWidget' => 'Angebote',
'newsSidebarWidget' => 'News', self::NEWS_SIDEBAR_WIDGET => 'News',
self::HOMEPAGE_PLANNABLE_TRIPS => 'Startseite: Aktuell planbare Reisen',
self::HOMEPAGE_POPULAR_TRIPS => 'Startseite: Beliebte Kulturreisen',
]; ];
@ -105,23 +111,256 @@ class SidebarWidget extends Model
return rtrim($ret, ", "); return rtrim($ret, ", ");
} }
public function getComponentLabel()
{
if (!$this->component) {
return 'Nur HTML';
}
return isset(self::$components[$this->component]) ? self::$components[$this->component] : $this->component;
}
public function isHomepageTripsComponent()
{
return in_array($this->component, [self::HOMEPAGE_PLANNABLE_TRIPS, self::HOMEPAGE_POPULAR_TRIPS]);
}
public function isNewsComponent()
{
return $this->component === self::NEWS_SIDEBAR_WIDGET;
}
public function isStructuredConfigComponent()
{
return $this->isHomepageTripsComponent() || $this->isNewsComponent();
}
public function getConfig()
{
$config = json_decode($this->html, true);
if (!is_array($config)) {
$config = [];
}
$config['page_ids'] = isset($config['page_ids']) && is_array($config['page_ids'])
? array_values(array_filter($config['page_ids']))
: [];
$config['new_page_ids'] = isset($config['new_page_ids']) && is_array($config['new_page_ids'])
? array_values(array_filter($config['new_page_ids']))
: [];
$config['new_badge_active'] = !empty($config['new_badge_active']);
$config['news_limit'] = isset($config['news_limit']) ? max(1, min(12, (int) $config['news_limit'])) : 3;
return $config;
}
public function getHomepageConfig()
{
return $this->getConfig();
}
public function getHomepagePageIds()
{
return $this->getHomepageConfig()['page_ids'];
}
public function getHomepageNewPageIds()
{
return array_map('intval', $this->getHomepageConfig()['new_page_ids']);
}
public function getHomepageNewBadgeActive()
{
return $this->getHomepageConfig()['new_badge_active'];
}
public function getHomepageNewsLimit()
{
return $this->getConfig()['news_limit'];
}
public static function getHomepageTravelPageOptions($selectedIds = [])
{
$selectedIds = array_map('intval', (array) $selectedIds);
$items = self::getHomepageTravelPageItems($selectedIds);
$groupedItems = [];
foreach ($items as $item) {
$groupedItems[$item['country_group']][] = $item;
}
uksort($groupedItems, function ($a, $b) {
return strnatcasecmp(self::getHomepageSortValue($a), self::getHomepageSortValue($b));
});
$ret = '';
foreach ($groupedItems as $countryGroup => $groupItems) {
$ret .= '<optgroup label="'.e($countryGroup).'">'."\n";
foreach ($groupItems as $item) {
$attr = $item['selected'] ? 'selected="selected"' : '';
$label = e($item['label']);
$ret .= '<option value="'.$item['id'].'" data-label="'.$label.'" '.$attr.'>'.$label.'</option>'."\n";
}
$ret .= '</optgroup>'."\n";
}
return $ret;
}
public static function getHomepageTravelPageItems($selectedIds = [])
{
$selectedIds = array_map('intval', (array) $selectedIds);
$pages = Page::with([
'travel_program_content.travel_program_countries.travel_country',
'travel_program_content.travel_country_content',
])
->where('status', 1)
->whereNotNull('travel_program')
->where('travel_program', '>', 0)
->whereHas('travel_program_content', function ($query) {
$query->where('status', 1);
})
->orderBy('title')
->get()
->unique('travel_program');
$items = [];
foreach ($pages as $page) {
$countryGroup = self::getHomepageTravelPageCountryLabel($page);
$items[] = [
'id' => (int) $page->id,
'label' => self::getHomepageTravelPageLabel($page),
'country_group' => $countryGroup,
'sort_title' => $page->title,
'selected' => in_array((int) $page->id, $selectedIds),
];
}
usort($items, function ($a, $b) {
$countryCompare = strnatcasecmp(
self::getHomepageSortValue($a['country_group']),
self::getHomepageSortValue($b['country_group'])
);
if ($countryCompare !== 0) {
return $countryCompare;
}
return strnatcasecmp(self::getHomepageSortValue($a['sort_title']), self::getHomepageSortValue($b['sort_title']));
});
return $items;
}
public function getHomepageSelectedTravelPageItems()
{
$selectedIds = array_map('intval', $this->getHomepagePageIds());
if (empty($selectedIds)) {
return [];
}
$newPageIds = $this->getHomepageNewPageIds();
$pages = Page::with([
'travel_program_content.travel_program_countries.travel_country',
'travel_program_content.travel_country_content',
])
->whereIn('id', $selectedIds)
->where('status', 1)
->whereHas('travel_program_content', function ($query) {
$query->where('status', 1);
})
->get()
->keyBy('id');
$items = [];
$usedTravelPrograms = [];
foreach ($selectedIds as $pageId) {
if (!$pages->has($pageId)) {
continue;
}
$page = $pages->get($pageId);
$travelProgramId = (int) $page->travel_program;
if (in_array($travelProgramId, $usedTravelPrograms)) {
continue;
}
$usedTravelPrograms[] = $travelProgramId;
$items[] = [
'id' => (int) $page->id,
'label' => self::getHomepageTravelPageLabel($page),
'is_new' => in_array((int) $page->id, $newPageIds),
];
}
return $items;
}
protected static function getHomepageTravelPageLabel(Page $page)
{
$country = self::getHomepageTravelPageCountryLabel($page);
$travelProgram = $page->travel_program_content;
$programId = $travelProgram ? $travelProgram->id : $page->travel_program;
return $country.' | Reise, '.$page->title.' (#'.$programId.')';
}
protected static function getHomepageTravelPageCountryLabel(Page $page)
{
$countryNames = self::getHomepageTravelPageCountryNames($page);
return !empty($countryNames) ? implode(', ', $countryNames) : 'Ohne Reiseland';
}
protected static function getHomepageTravelPageCountryNames(Page $page)
{
$travelProgram = $page->travel_program_content;
$countryNames = [];
if ($travelProgram && $travelProgram->travel_program_countries) {
foreach ($travelProgram->travel_program_countries as $programCountry) {
if ($programCountry->travel_country && $programCountry->travel_country->name) {
$countryNames[] = $programCountry->travel_country->name;
}
}
}
if (empty($countryNames) && $travelProgram && $travelProgram->travel_country_content) {
$countryNames[] = $travelProgram->travel_country_content->name;
}
$countryNames = array_unique($countryNames);
usort($countryNames, function ($a, $b) {
return strnatcasecmp(self::getHomepageSortValue($a), self::getHomepageSortValue($b));
});
return $countryNames;
}
protected static function getHomepageSortValue($value)
{
return str_replace(
['Ä', 'Ö', 'Ü', 'ä', 'ö', 'ü', 'ß'],
['Ae', 'Oe', 'Ue', 'ae', 'oe', 'ue', 'ss'],
(string) $value
);
}
/* /*
* public function getVotesDetailAttribute($details) * public function getVotesDetailAttribute($details)
{ {
return json_decode($details, true); * return json_decode($details, true);
}
then when you will call $store->votes_detail you will get the expected result.
After that you can use mutators to convert an array back to JSON when it is saved back in the DB. Define the method setVotesDetailAttribute($value) as follows:
public function setVotesDetailsAttribute($value)
{
$this->attributes['votes_detail'] = json_encode($value);
} }
* then when you will call $store->votes_detail you will get the expected result.
*
* After that you can use mutators to convert an array back to JSON when it is saved back in the DB. Define the method setVotesDetailAttribute($value) as follows:
*
* public function setVotesDetailsAttribute($value)
* {
* $this->attributes['votes_detail'] = json_encode($value);
* }
*/ */
} }

View file

@ -295,6 +295,16 @@ class TravelProgram extends Model
return $this->hasOne(TravelProgramCountry::class, 'program_id'); return $this->hasOne(TravelProgramCountry::class, 'program_id');
} }
public function travel_program_countries()
{
return $this->hasMany(TravelProgramCountry::class, 'program_id');
}
public function travel_country_content()
{
return $this->belongsTo(TravelCountry::class, 'travel_country');
}
public function travel_program_destination() public function travel_program_destination()
{ {
//return $this->hasOne(TravelProgramDestination::class, 'program_id'); //return $this->hasOne(TravelProgramDestination::class, 'program_id');

View file

@ -38,15 +38,82 @@
<div class="form-row"> <div class="form-row">
<div class="form-group col-sm-12"> <div class="form-group col-sm-12">
<label class="form-label" for="component">{{ __('Komponente') }}</label> <label class="form-label" for="component">{{ __('Komponente') }}</label>
<select class="selectpicker" data-style="btn-default" name="component"> <select class="selectpicker" data-style="btn-default" name="component" id="component">
{!! \App\Models\SidebarWidget::getComponentsOptions($widget->component) !!} {!! \App\Models\SidebarWidget::getComponentsOptions($widget->component) !!}
</select> </select>
</div> </div>
</div> </div>
<div class="form-row"> <div class="form-row" data-config-panel="html" style="{{ $widget->isStructuredConfigComponent() ? 'display:none;' : '' }}">
<div class="form-group col-sm-12"> <div class="form-group col-sm-12">
<label class="form-label" for="html">{{ __('HTML') }}</label> <label class="form-label" for="html">{{ __('HTML') }}</label>
{{ Form::textarea('html', $widget->html, ['class' => 'form-control']) }} {{ Form::textarea('html', $widget->html, ['class' => 'form-control']) }}
<small class="form-text text-muted">
Wird nur genutzt, wenn keine feste Komponente ausgewählt ist oder die Komponente eigene HTML-Inhalte erwartet.
</small>
</div>
</div>
<div data-config-panel="homepage-trips" style="{{ $widget->isHomepageTripsComponent() ? '' : 'display:none;' }}">
<div class="alert alert-info">
Diese Auswahl steuert die Startseiten-Bereiche „Aktuell planbare Reisen" und „Beliebte Kulturreisen".
Die Reihenfolge entspricht der Reihenfolge der ausgewählten Einträge.
</div>
<div class="form-row">
<div class="form-group col-sm-12">
<label class="form-label" for="homepage_page_picker">{{ __('Reise hinzufügen') }}</label>
<div class="input-group">
<select class="selectpicker form-control" data-style="btn-default" name="homepage_page_picker" id="homepage_page_picker" data-live-search="true">
<option value="">{{ __('Bitte Reise wählen') }}</option>
{!! \App\Models\SidebarWidget::getHomepageTravelPageOptions($widget->getHomepagePageIds()) !!}
</select>
<span class="input-group-append">
<button class="btn btn-primary" type="button" id="homepage_add_page">{{ __('hinzufügen') }}</button>
</span>
</div>
<small class="form-text text-muted">
Wenn keine Reisen hinzugefügt sind, nutzt die DEV-Startseite die automatische Prioritätslogik.
</small>
</div>
</div>
<div class="form-row">
<div class="form-group col-sm-12">
<label class="form-label">{{ __('Ausgewählte Reisen') }}</label>
<div class="list-group" id="homepage_selected_pages">
@foreach($widget->getHomepageSelectedTravelPageItems() as $selectedPage)
<div class="list-group-item d-flex align-items-center justify-content-between" data-page-id="{{ $selectedPage['id'] }}">
<input type="hidden" name="homepage_page_ids[]" value="{{ $selectedPage['id'] }}">
<span class="mr-3">
<span class="badge badge-secondary mr-2" data-page-number>{{ $loop->iteration }}</span>
{{ $selectedPage['label'] }}
</span>
<span class="d-flex align-items-center">
<label class="custom-control custom-checkbox mb-0 mr-3">
<input type="checkbox" class="custom-control-input" name="homepage_new_page_ids[]" value="{{ $selectedPage['id'] }}" {{ $selectedPage['is_new'] ? 'checked' : '' }}>
<span class="custom-control-label">
<span class="badge badge-warning">{{ __('Neu') }}</span>
</span>
</label>
<span class="btn-group btn-group-sm mr-2" role="group" aria-label="{{ __('Reihenfolge ändern') }}">
<button class="btn btn-outline-secondary" type="button" data-move-page="up">{{ __('hoch') }}</button>
<button class="btn btn-outline-secondary" type="button" data-move-page="down">{{ __('runter') }}</button>
</span>
<button class="btn btn-sm btn-outline-danger" type="button" data-remove-page>{{ __('entfernen') }}</button>
</span>
</div>
@endforeach
</div>
</div>
</div>
</div>
<div data-config-panel="news" style="{{ $widget->isNewsComponent() ? '' : 'display:none;' }}">
<div class="alert alert-info">
Die Reisenews werden weiterhin automatisch aus dem News-Bereich geladen. Hier wird nur gesteuert,
wie viele Einträge in der Sidebar erscheinen.
</div>
<div class="form-row">
<div class="form-group col-sm-3">
<label class="form-label" for="homepage_news_limit">{{ __('Anzahl Reisenews') }}</label>
<input type="number" min="1" max="12" class="form-control" name="homepage_news_limit" id="homepage_news_limit" value="{{ $widget->getHomepageNewsLimit() }}">
</div>
</div> </div>
</div> </div>
<div class="form-row"> <div class="form-row">
@ -67,6 +134,97 @@
{!! Form::close() !!} {!! Form::close() !!}
<script>
$(function () {
var componentSelect = $('#component');
var panels = $('[data-config-panel]');
var homepageTripComponents = ['homepagePlannableTrips', 'homepagePopularTrips'];
var pagePicker = $('#homepage_page_picker');
var selectedPages = $('#homepage_selected_pages');
function updateConfigPanels() {
var component = componentSelect.val();
panels.hide();
if (homepageTripComponents.indexOf(component) !== -1) {
$('[data-config-panel="homepage-trips"]').show();
} else if (component === 'newsSidebarWidget') {
$('[data-config-panel="news"]').show();
} else {
$('[data-config-panel="html"]').show();
}
$('.selectpicker').selectpicker('refresh');
}
function addSelectedPage(pageId, label, isNew) {
if (!pageId || selectedPages.find('[data-page-id="' + pageId + '"]').length) {
return;
}
var item = $('<div class="list-group-item d-flex align-items-center justify-content-between"></div>');
var title = $('<span class="mr-3"></span>');
var controls = $('<span class="d-flex align-items-center"></span>');
var checkboxId = 'homepage_new_page_' + pageId;
var newLabel = $('<label class="custom-control custom-checkbox mb-0 mr-3"></label>');
var newCheckbox = $('<input type="checkbox" class="custom-control-input" name="homepage_new_page_ids[]">')
.attr('id', checkboxId)
.val(pageId);
var moveButtons = $('<span class="btn-group btn-group-sm mr-2" role="group" aria-label="{{ __('Reihenfolge ändern') }}"></span>');
var removeButton = $('<button class="btn btn-sm btn-outline-danger" type="button" data-remove-page>{{ __('entfernen') }}</button>');
if (isNew) {
newCheckbox.prop('checked', true);
}
item.attr('data-page-id', pageId);
item.append($('<input type="hidden" name="homepage_page_ids[]">').val(pageId));
title.append($('<span class="badge badge-secondary mr-2" data-page-number></span>'));
title.append(document.createTextNode(label));
newLabel.append(newCheckbox);
newLabel.append($('<span class="custom-control-label"><span class="badge badge-warning">{{ __('Neu') }}</span></span>'));
controls.append(newLabel);
moveButtons.append($('<button class="btn btn-outline-secondary" type="button" data-move-page="up">{{ __('hoch') }}</button>'));
moveButtons.append($('<button class="btn btn-outline-secondary" type="button" data-move-page="down">{{ __('runter') }}</button>'));
controls.append(moveButtons);
controls.append(removeButton);
item.append(title);
item.append(controls);
selectedPages.append(item);
updateSelectedPageNumbers();
}
function updateSelectedPageNumbers() {
selectedPages.find('[data-page-id]').each(function (index) {
$(this).find('[data-page-number]').text(index + 1);
});
}
componentSelect.on('changed.bs.select change', updateConfigPanels);
$('#homepage_add_page').on('click', function () {
var selectedOption = pagePicker.find('option:selected');
addSelectedPage(selectedOption.val(), selectedOption.data('label') || selectedOption.text(), false);
});
selectedPages.on('click', '[data-remove-page]', function () {
$(this).closest('[data-page-id]').remove();
updateSelectedPageNumbers();
});
selectedPages.on('click', '[data-move-page]', function () {
var item = $(this).closest('[data-page-id]');
if ($(this).data('move-page') === 'up') {
item.prev('[data-page-id]').before(item);
} else {
item.next('[data-page-id]').after(item);
}
updateSelectedPageNumbers();
});
updateSelectedPageNumbers();
updateConfigPanels();
});
</script>
{{-- {{--
<!-- Modal template --> <!-- Modal template -->
<div class="modal fade" id="modals-class"> <div class="modal fade" id="modals-class">

View file

@ -9,14 +9,15 @@
<div class="card"> <div class="card">
<div class="card-datatable table-responsive py-2"> <div class="card-datatable table-responsive py-2">
<div class="mr-4 mb-2 text-right"> <div class="mr-4 mb-2 text-right">
<a href="{{ route('cms_sidebar_detail', ['new']) }}" class="btn btn-sm btn-primary">Neues Sidbar Widget anlegen</a> <a href="{{ route('cms_sidebar_detail', ['new']) }}" class="btn btn-sm btn-primary">Neues Sidebar-Widget anlegen</a>
</div> </div>
<table class="datatables-feedbacks table table-striped table-bordered"> <table class="datatables-feedbacks table table-striped table-bordered">
<thead> <thead>
<tr> <tr>
<th style="max-width: 60px;">&nbsp;</th> <th style="max-width: 60px;">&nbsp;</th>
<th>{{__('Name')}}</th> <th>{{__('Name')}}</th>
<th>{{__('Sichbar')}}</th> <th>{{__('Sichtbar auf')}}</th>
<th>{{__('Komponente')}}</th>
<th>{{__('Pos.')}}</th> <th>{{__('Pos.')}}</th>
<th>{{__('sichtbar')}}</th> <th>{{__('sichtbar')}}</th>
<th></th> <th></th>
@ -34,10 +35,11 @@
<a href="{{ route('cms_sidebar_detail', [$value->id]) }}">{{ $value->name }}</a> <a href="{{ route('cms_sidebar_detail', [$value->id]) }}">{{ $value->name }}</a>
</td> </td>
<td>{{ $value->getShowsAtString() }}</td> <td>{{ $value->getShowsAtString() }}</td>
<td>{{ $value->getComponentLabel() }}</td>
<td> <td>
{{ $value->pos }} {{ $value->pos }}
</td> </td>
<td data-sort="{{ $value->status }}"> <td data-sort="{{ $value->active }}">
@if($value->active) @if($value->active)
<span class="badge badge-pill badge-success"><i class="fa fa-check"></i></span> <span class="badge badge-pill badge-success"><i class="fa fa-check"></i></span>
@else @else
@ -52,7 +54,7 @@
</tbody> </tbody>
</table> </table>
<div class="mt-4 col"> <div class="mt-4 col">
<a href="{{ route('cms_sidebar_detail', ['new']) }}" class="btn btn-sm btn-primary">Neues Sidbar Widget anlegen</a> <a href="{{ route('cms_sidebar_detail', ['new']) }}" class="btn btn-sm btn-primary">Neues Sidebar-Widget anlegen</a>
</div> </div>
</div> </div>
<script> <script>