23-01-2026

This commit is contained in:
Kevin Adametz 2026-01-23 17:34:40 +01:00
parent 8fd1f4d451
commit 389d5d1820
59 changed files with 9642 additions and 883 deletions

View file

@ -0,0 +1,185 @@
<?php
namespace App\Http\Controllers\API;
use App\Http\Controllers\Controller;
use App\Services\NavigationTreeService;
use Illuminate\Http\JsonResponse;
class NavigationController extends Controller
{
protected $navigationService;
public function __construct(NavigationTreeService $navigationService)
{
$this->navigationService = $navigationService;
}
/**
* Gibt den kompletten Navigationsbaum zurück
*
* @return JsonResponse
*/
public function getNavigationTree(): JsonResponse
{
try {
$tree = $this->navigationService->getNavigationTree();
return response()->json([
'success' => true,
'data' => $tree,
'meta' => [
'total_nodes' => $this->navigationService->countNodes($tree),
'generated_at' => now()->toIso8601String()
]
]);
} catch (\Exception $e) {
return response()->json([
'success' => false,
'error' => $e->getMessage()
], 500);
}
}
/**
* Gibt einen spezifischen Teil des Navigationsbaums zurück
*
* @param int $rootId Die ID des Root-Knotens
* @return JsonResponse
*/
public function getNavigationSubTree(int $rootId): JsonResponse
{
try {
$tree = $this->navigationService->getNavigationSubTree($rootId);
if (!$tree) {
return response()->json([
'success' => false,
'error' => 'Navigation node not found'
], 404);
}
return response()->json([
'success' => true,
'data' => $tree,
'meta' => [
'total_nodes' => $this->navigationService->countNodes([$tree]),
'generated_at' => now()->toIso8601String()
]
]);
} catch (\Exception $e) {
return response()->json([
'success' => false,
'error' => $e->getMessage()
], 500);
}
}
/**
* Gibt eine flache Liste aller Navigationspunkte zurück (ohne Hierarchie)
*
* @return JsonResponse
*/
public function getFlatNavigationList(): JsonResponse
{
try {
$list = $this->navigationService->getFlatNavigationList();
return response()->json([
'success' => true,
'data' => $list,
'meta' => [
'total_nodes' => count($list),
'generated_at' => now()->toIso8601String()
]
]);
} catch (\Exception $e) {
return response()->json([
'success' => false,
'error' => $e->getMessage()
], 500);
}
}
/**
* Gibt nur die aktiven Navigationspunkte zurück
*
* @return JsonResponse
*/
public function getActiveNavigationTree(): JsonResponse
{
try {
$tree = $this->navigationService->getNavigationTree(true);
return response()->json([
'success' => true,
'data' => $tree,
'meta' => [
'total_nodes' => $this->navigationService->countNodes($tree),
'generated_at' => now()->toIso8601String()
]
]);
} catch (\Exception $e) {
return response()->json([
'success' => false,
'error' => $e->getMessage()
], 500);
}
}
/**
* Gibt den Breadcrumb-Pfad für eine bestimmte Seite zurück
*
* @param int $pageId
* @return JsonResponse
*/
public function getBreadcrumb(int $pageId): JsonResponse
{
try {
$breadcrumb = $this->navigationService->getBreadcrumb($pageId);
if (empty($breadcrumb)) {
return response()->json([
'success' => false,
'error' => 'Page not found'
], 404);
}
return response()->json([
'success' => true,
'data' => $breadcrumb,
'meta' => [
'depth' => count($breadcrumb),
'generated_at' => now()->toIso8601String()
]
]);
} catch (\Exception $e) {
return response()->json([
'success' => false,
'error' => $e->getMessage()
], 500);
}
}
/**
* Löscht den Navigation-Cache
*
* @return JsonResponse
*/
public function clearCache(): JsonResponse
{
try {
$this->navigationService->clearCache();
return response()->json([
'success' => true,
'message' => 'Navigation cache cleared successfully'
]);
} catch (\Exception $e) {
return response()->json([
'success' => false,
'error' => $e->getMessage()
], 500);
}
}
}

View file

@ -8,12 +8,13 @@ use App\Models\News;
use App\Models\Page;
use Carbon\Carbon;
use IqContent\LaravelFilemanager\Lfm;
use Illuminate\Support\Str;
use Request;
class CMSNewsController extends Controller
{
/*
/*
* Create a new controller instance.
*
* @return void
@ -21,29 +22,25 @@ class CMSNewsController extends Controller
public function __construct()
{
$this->middleware(['admin', '2fa']);
}
public function index()
{
$data = [
'news' => News::all(),//News::where('lvl', 1)->get(),
'news' => News::all(), //News::where('lvl', 1)->get(),
];
return view('cms.news.index', $data);
}
public function detail($id)
{
if($id === "new") {
if ($id === "new") {
$news = new News();
$id = 'new';
$news->status = 1;
$news->content_new = "";
}else{
} else {
$news = News::findOrFail($id);
$id = $news->id;
}
@ -53,26 +50,25 @@ class CMSNewsController extends Controller
'lfm_helper' => app(Lfm::class),
];
return view('cms.news.detail', $data);
}
public function store($id)
{
$data = Request::all();
if($id === "new") {
if ($id === "new") {
$news = new News();
$news->model = 'news';
$news->owner_second = 0;
$news->show_in_navi = 1;
$news->catalog_id = 1;
}else{
} else {
$news = News::findOrFail($id);
}
$news->title = $data['title'];
$news->status = isset($data['status']) ? true : false;
$news->slug = $data['slug'];
$news->slug = Str::slug($data['slug']);
$news->date = $data['date'];
$news->content_new = $data['content_new'];
$news->box_body = $data['image'];
@ -80,37 +76,37 @@ class CMSNewsController extends Controller
$news->pagetitle = $data['pagetitle'];
$news->keywords = $data['keywords'];
$news->order = (new Carbon($news->date))->format('Ymd')*-1;
$news->order = (new Carbon($news->date))->format('Ymd') * -1;
$root_news = News::where('cms_settings', 'news_root')->first();
if($id != $root_news->id){
if ($id != $root_news->id) {
//root ID = 3126
$news->lvl = 1;
$news->owner = $root_news->id;
$news->parent_id = $root_news->id;
$news->tree_root = $root_news->id;
if($first_news = $root_news->children->first()){
if ($first_news = $root_news->children->first()) {
$news->lft = $first_news->lft;
$news->rgt = $first_news->rgt;
}else{
$news->lft = $root_news->lft +1;
$news->rgt = $root_news->lft +2;
} else {
$news->lft = $root_news->lft + 1;
$news->rgt = $root_news->lft + 2;
}
}
$news->save();
\Session()->flash('alert-save', '1');
return redirect(route('cms_news_detail', [$news->id]));
}
public function delete($id){
public function delete($id)
{
$news = News::findOrFail($id);
//TODO
//check for delete, only delete lvl 2 .,...?
if ($news->lvl != 1){
if ($news->lvl != 1) {
abort(404);
die();
}
@ -119,6 +115,4 @@ class CMSNewsController extends Controller
\Session()->flash('alert-success', __('News gelöscht'));
return redirect(route('cms_news'));
}
}

View file

@ -0,0 +1,165 @@
<?php
namespace App\Http\Controllers;
use App\Services\NavigationTreeService;
use Illuminate\Http\Request;
use Illuminate\Http\JsonResponse;
class NavigationTreeController extends Controller
{
protected $navigationService;
public function __construct(NavigationTreeService $navigationService)
{
$this->navigationService = $navigationService;
}
/**
* Zeigt die Navigationsbaum-Übersicht
*
* @return \Illuminate\View\View
*/
public function index()
{
return view('navigation.index');
}
/**
* Gibt die Navigationsbaum-Daten als JSON zurück (Frontend-Struktur)
*
* @param Request $request
* @return JsonResponse
*/
public function getData(Request $request): JsonResponse
{
try {
$includeHidden = $request->get('include_hidden', true);
$tree = $this->navigationService->getFrontendNavigationTree($includeHidden);
return response()->json([
'success' => true,
'data' => $tree,
'meta' => [
'total_nodes' => $this->navigationService->countNodes($tree),
'include_hidden' => $includeHidden,
'structure' => 'frontend'
]
]);
} catch (\Exception $e) {
return response()->json([
'success' => false,
'error' => $e->getMessage()
], 500);
}
}
/**
* Sucht im Navigationsbaum
*
* @param Request $request
* @return JsonResponse
*/
public function search(Request $request): JsonResponse
{
try {
$query = $request->get('query', '');
$flatList = $this->navigationService->getFlatNavigationList();
// Filtere nach Suchbegriff
$results = array_filter($flatList, function ($node) use ($query) {
return stripos($node['title'], $query) !== false
|| stripos($node['slug'], $query) !== false
|| stripos($node['url'], $query) !== false;
});
return response()->json([
'success' => true,
'data' => array_values($results),
'meta' => [
'query' => $query,
'total_results' => count($results)
]
]);
} catch (\Exception $e) {
return response()->json([
'success' => false,
'error' => $e->getMessage()
], 500);
}
}
/**
* Exportiert den Navigationsbaum als JSON-Datei
*
* @param Request $request
* @return \Illuminate\Http\Response
*/
public function export(Request $request)
{
try {
$includeHidden = $request->get('include_hidden', true);
$tree = $this->navigationService->getFrontendNavigationTree($includeHidden);
$filename = 'navigation-tree-frontend-' . date('Y-m-d-His') . '.json';
$json = json_encode($tree, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
return response($json)
->header('Content-Type', 'application/json')
->header('Content-Disposition', 'attachment; filename="' . $filename . '"');
} catch (\Exception $e) {
return back()->with('error', 'Export fehlgeschlagen: ' . $e->getMessage());
}
}
/**
* Löscht den Cache
*
* @return \Illuminate\Http\RedirectResponse
*/
public function clearCache()
{
try {
$this->navigationService->clearCache();
return back()->with('success', 'Navigation-Cache erfolgreich gelöscht');
} catch (\Exception $e) {
return back()->with('error', 'Cache-Löschung fehlgeschlagen: ' . $e->getMessage());
}
}
/**
* Zeigt die Statistiken
*
* @return JsonResponse
*/
public function stats(): JsonResponse
{
try {
$allTree = $this->navigationService->getNavigationTree(false);
$activeTree = $this->navigationService->getNavigationTree(true);
$flatList = $this->navigationService->getFlatNavigationList();
// Zähle verschiedene Typen
$stats = [
'total_pages' => count($flatList),
'total_nodes' => $this->navigationService->countNodes($allTree),
'active_nodes' => $this->navigationService->countNodes($activeTree),
'inactive_nodes' => $this->navigationService->countNodes($allTree) - $this->navigationService->countNodes($activeTree),
'travel_programs' => count(array_filter($flatList, fn($n) => $n['is_travel_program'])),
'fewo_lodgings' => count(array_filter($flatList, fn($n) => $n['is_fewo_lodging'])),
'country_pages' => count(array_filter($flatList, fn($n) => $n['is_country_page'])),
];
return response()->json([
'success' => true,
'data' => $stats
]);
} catch (\Exception $e) {
return response()->json([
'success' => false,
'error' => $e->getMessage()
], 500);
}
}
}

View file

@ -0,0 +1,391 @@
<?php
namespace App\Http\Controllers;
use App\Models\NewsletterContact;
use App\Models\NewsletterLog;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Artisan;
use Illuminate\Support\Facades\Validator;
use Yajra\DataTables\Facades\DataTables;
use Maatwebsite\Excel\Facades\Excel;
use App\Exports\NewsletterExport;
use Carbon\Carbon;
class NewsletterController extends Controller
{
public function __construct()
{
$this->middleware(['admin', '2fa']);
}
/**
* Liste aller Newsletter-Kontakte
*/
public function index()
{
$data = [
'statistics' => $this->getStatistics(),
];
return view('newsletter.index', $data);
}
/**
* DataTables Daten für die Liste
*/
public function getDatatable(Request $request)
{
$query = NewsletterContact::query()
->select([
'id',
'email',
'firstname',
'lastname',
'group_kulturreisen',
'group_ferienwohnungen',
'status',
'source',
'total_bookings_kulturreisen',
'total_bookings_ferienwohnungen',
'last_booking_at',
'last_travel_end_date',
'created_at',
]);
// Filter nach Gruppe
if ($request->has('group') && $request->group != '') {
if ($request->group == 'kulturreisen') {
$query->where('group_kulturreisen', true);
} elseif ($request->group == 'ferienwohnungen') {
$query->where('group_ferienwohnungen', true);
}
}
// Filter nach Status
if ($request->has('status') && $request->status != '') {
$query->where('status', $request->status);
}
// Filter nach Source
if ($request->has('source') && $request->source != '') {
$query->where('source', $request->source);
}
// Filter nach Datum der letzten Buchung (von)
if ($request->has('travel_from') && $request->travel_from != '') {
$query->whereDate('last_travel_end_date', '>=', Carbon::parse($request->travel_from)->format('Y-m-d H:i:s'));
}
// Filter nach Datum der letzten Buchung (bis)
if ($request->has('travel_to') && $request->travel_to != '') {
$query->whereDate('last_travel_end_date', '<=', Carbon::parse($request->travel_to)->format('Y-m-d H:i:s'));
}
return DataTables::of($query)
->addColumn('full_name', function ($contact) {
return $contact->full_name ?: '-';
})
->addColumn('groups', function ($contact) {
$html = '';
if ($contact->group_kulturreisen) {
$html .= '<span class="badge badge-info">Kulturreisen</span> ';
}
if ($contact->group_ferienwohnungen) {
$html .= '<span class="badge badge-primary">Ferienwohnungen</span>';
}
return $html ?: '-';
})
->addColumn('status_badge', function ($contact) {
return '<span class="badge badge-' . $contact->status_color . '">' . $contact->status_label . '</span>';
})
->addColumn('total_bookings', function ($contact) {
$html = '';
if ($contact->total_bookings_kulturreisen > 0) {
$html .= '<span class="badge badge-secondary">K: ' . $contact->total_bookings_kulturreisen . '</span> ';
}
if ($contact->total_bookings_ferienwohnungen > 0) {
$html .= '<span class="badge badge-secondary">F: ' . $contact->total_bookings_ferienwohnungen . '</span>';
}
return $html ?: '0';
})
->addColumn('last_booking', function ($contact) {
return $contact->last_booking_at ? $contact->last_booking_at->format('d.m.Y') : '-';
})
->addColumn('last_travel', function ($contact) {
return $contact->last_travel_end_date ? $contact->last_travel_end_date->format('d.m.Y') : '-';
})
->addColumn('source_label', function ($contact) {
return $contact->source_label;
})
->addColumn('created', function ($contact) {
return $contact->created_at->format('d.m.Y');
})
->addColumn('actions', function ($contact) {
$html = '<div class="btn-group">';
$html .= '<a href="' . route('newsletter.detail', $contact->id) . '" class="btn btn-sm btn-info"><i class="fa fa-eye"></i></a>';
$html .= '<a href="' . route('newsletter.edit', $contact->id) . '" class="btn btn-sm btn-primary"><i class="fa fa-edit"></i></a>';
$html .= '</div>';
return $html;
})
->rawColumns(['groups', 'status_badge', 'total_bookings', 'actions'])
->make(true);
}
/**
* Detailansicht eines Kontakts
*/
public function detail($id)
{
$contact = NewsletterContact::with(['customer', 'travel_user', 'logs.user'])
->findOrFail($id);
$data = [
'contact' => $contact,
];
return view('newsletter.detail', $data);
}
/**
* Formular zum Bearbeiten eines Kontakts
*/
public function edit($id)
{
if ($id === 'new') {
$contact = new NewsletterContact();
$contact->status = NewsletterContact::STATUS_ACTIVE;
$contact->source = NewsletterContact::SOURCE_MANUAL;
} else {
$contact = NewsletterContact::findOrFail($id);
}
$data = [
'contact' => $contact,
'id' => $id,
];
return view('newsletter.edit', $data);
}
/**
* Speichern eines Kontakts
*/
public function store($id, Request $request)
{
$rules = [
'email' => 'required|email',
'status' => 'required|in:' . implode(',', [
NewsletterContact::STATUS_ACTIVE,
NewsletterContact::STATUS_INACTIVE,
NewsletterContact::STATUS_UNSUBSCRIBED,
NewsletterContact::STATUS_BOUNCED,
]),
];
$validator = Validator::make($request->all(), $rules);
if ($validator->fails()) {
return back()
->withErrors($validator)
->withInput();
}
if ($id === 'new') {
$contact = new NewsletterContact();
$isNew = true;
} else {
$contact = NewsletterContact::findOrFail($id);
$isNew = false;
}
// Speichere alte Werte für Log
$oldStatus = $contact->status;
$oldGroups = [
'kulturreisen' => $contact->group_kulturreisen,
'ferienwohnungen' => $contact->group_ferienwohnungen,
];
$contact->email = strtolower(trim($request->email));
$contact->firstname = $request->firstname;
$contact->lastname = $request->lastname;
$contact->status = $request->status;
$contact->source = $request->source ?? NewsletterContact::SOURCE_MANUAL;
$contact->group_kulturreisen = $request->has('group_kulturreisen');
$contact->group_ferienwohnungen = $request->has('group_ferienwohnungen');
$contact->notes = $request->notes;
if ($isNew) {
$contact->subscribed_at = now();
}
$contact->save();
// Log erstellen
if ($isNew) {
$contact->logs()->create([
'action' => 'subscribed',
'description' => 'Kontakt manuell erstellt',
'user_id' => auth()->id(),
]);
} else {
// Status geändert?
if ($oldStatus !== $contact->status) {
$contact->logs()->create([
'action' => 'status_changed',
'description' => 'Status geändert von ' . NewsletterContact::$statusLabels[$oldStatus] . ' zu ' . $contact->status_label,
'user_id' => auth()->id(),
]);
}
// Gruppen geändert?
if (
$oldGroups['kulturreisen'] !== $contact->group_kulturreisen ||
$oldGroups['ferienwohnungen'] !== $contact->group_ferienwohnungen
) {
$contact->logs()->create([
'action' => 'group_changed',
'description' => 'Gruppenzugehörigkeit geändert',
'user_id' => auth()->id(),
]);
}
}
\Session()->flash('alert-success', $isNew ? 'Kontakt erstellt' : 'Kontakt aktualisiert');
return redirect()->route('newsletter.detail', $contact->id);
}
/**
* Kontakt löschen (soft delete)
*/
public function delete($id)
{
$contact = NewsletterContact::findOrFail($id);
$contact->logs()->create([
'action' => 'unsubscribed',
'description' => 'Kontakt gelöscht',
'user_id' => auth()->id(),
]);
$contact->delete();
\Session()->flash('alert-success', 'Kontakt gelöscht');
return redirect()->route('newsletter.index');
}
/**
* Kontakt abmelden
*/
public function unsubscribe($id, Request $request)
{
$contact = NewsletterContact::findOrFail($id);
$contact->unsubscribe($request->reason);
\Session()->flash('alert-success', 'Kontakt abgemeldet');
return back();
}
/**
* Kontakt wieder aktivieren
*/
public function resubscribe($id)
{
$contact = NewsletterContact::findOrFail($id);
$contact->resubscribe();
\Session()->flash('alert-success', 'Kontakt wieder aktiviert');
return back();
}
/**
* Synchronisation starten
*/
public function sync(Request $request)
{
$type = $request->get('type', 'all');
$force = $request->has('force');
$output = [];
if ($type === 'all' || $type === 'kulturreisen') {
Artisan::call('newsletter:sync-kulturreisen', $force ? ['--force' => true] : []);
$output['kulturreisen'] = Artisan::output();
}
if ($type === 'all' || $type === 'ferienwohnungen') {
Artisan::call('newsletter:sync-ferienwohnungen', $force ? ['--force' => true] : []);
$output['ferienwohnungen'] = Artisan::output();
}
\Session()->flash('alert-success', 'Synchronisation abgeschlossen');
return back()->with('sync_output', $output);
}
/**
* Export von Kontakten
*/
public function export(Request $request)
{
$query = NewsletterContact::query();
// Filter nach Gruppe
if ($request->has('group') && $request->group != '') {
if ($request->group == 'kulturreisen') {
$query->where('group_kulturreisen', true);
} elseif ($request->group == 'ferienwohnungen') {
$query->where('group_ferienwohnungen', true);
}
}
// Filter nach Status
if ($request->has('status') && $request->status != '') {
$query->where('status', $request->status);
}
// Filter nach Source
if ($request->has('source') && $request->source != '') {
$query->where('source', $request->source);
}
// Filter nach Datum der letzten Reise (von)
if ($request->has('travel_from') && $request->travel_from != '') {
$query->whereDate('last_travel_end_date', '>=', Carbon::parse($request->travel_from)->format('Y-m-d H:i:s'));
}
// Filter nach Datum der letzten Reise (bis)
if ($request->has('travel_to') && $request->travel_to != '') {
$query->whereDate('last_travel_end_date', '<=', Carbon::parse($request->travel_to)->format('Y-m-d H:i:s'));
}
$contacts = $query->get();
// Dateiname mit Datum und Filter-Infos
$group = $request->get('group', 'all');
$status = $request->get('status', 'all');
$filename = 'newsletter_' . $group . '_' . $status . '_' . date('Y-m-d') . '.csv';
return Excel::download(new NewsletterExport($contacts), $filename);
}
/**
* Statistiken für Dashboard
*/
private function getStatistics()
{
return [
'total' => NewsletterContact::count(),
'active' => NewsletterContact::where('status', NewsletterContact::STATUS_ACTIVE)->count(),
'kulturreisen' => NewsletterContact::where('group_kulturreisen', true)->count(),
'ferienwohnungen' => NewsletterContact::where('group_ferienwohnungen', true)->count(),
'with_bookings' => NewsletterContact::withBookings()->count(),
'multiple_bookers' => NewsletterContact::multipleBookers()->count(),
'unsubscribed' => NewsletterContact::where('status', NewsletterContact::STATUS_UNSUBSCRIBED)->count(),
'last_sync' => NewsletterContact::max('last_synced_at'),
];
}
}