23-01-2026
This commit is contained in:
parent
8fd1f4d451
commit
389d5d1820
59 changed files with 9642 additions and 883 deletions
122
app/Console/Commands/CleanupNewsletterBlockedEmails.php
Normal file
122
app/Console/Commands/CleanupNewsletterBlockedEmails.php
Normal file
|
|
@ -0,0 +1,122 @@
|
|||
<?php
|
||||
|
||||
namespace App\Console\Commands;
|
||||
|
||||
use App\Models\NewsletterContact;
|
||||
use Illuminate\Console\Command;
|
||||
|
||||
class CleanupNewsletterBlockedEmails extends Command
|
||||
{
|
||||
/**
|
||||
* The name and signature of the console command.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $signature = 'newsletter:cleanup-blocked-emails {--dry-run : Nur anzeigen, ohne zu löschen}';
|
||||
|
||||
/**
|
||||
* The console description of the command.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $description = 'Entfernt Newsletter-Kontakte mit blockierten E-Mail-Adressen (Alias/Proxy von Buchungsplattformen)';
|
||||
|
||||
/**
|
||||
* Execute the console command.
|
||||
*
|
||||
* @return int
|
||||
*/
|
||||
public function handle()
|
||||
{
|
||||
$this->info('Suche nach blockierten E-Mail-Adressen...');
|
||||
|
||||
$dryRun = $this->option('dry-run');
|
||||
|
||||
// Liste der blockierten Domains
|
||||
$blockedDomains = [
|
||||
'@guest.booking.com',
|
||||
'@messages.homeaway.com',
|
||||
'@fewo.check24.de',
|
||||
'@booking.com',
|
||||
'@homeaway.com',
|
||||
'@check24.de',
|
||||
'@partner.booking.com',
|
||||
];
|
||||
|
||||
$stats = [
|
||||
'found' => 0,
|
||||
'deleted' => 0,
|
||||
];
|
||||
|
||||
// Hole alle Newsletter-Kontakte
|
||||
$contacts = NewsletterContact::all();
|
||||
|
||||
$this->info("Prüfe {$contacts->count()} Kontakte...");
|
||||
|
||||
$bar = $this->output->createProgressBar($contacts->count());
|
||||
$bar->start();
|
||||
|
||||
foreach ($contacts as $contact) {
|
||||
$emailLower = strtolower($contact->email);
|
||||
$isBlockedEmail = false;
|
||||
|
||||
foreach ($blockedDomains as $domain) {
|
||||
if (str_ends_with($emailLower, strtolower($domain))) {
|
||||
$isBlockedEmail = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if ($isBlockedEmail) {
|
||||
$stats['found']++;
|
||||
|
||||
if ($dryRun) {
|
||||
$this->newLine();
|
||||
$this->warn("Würde löschen: {$contact->email} (ID: {$contact->id})");
|
||||
} else {
|
||||
// Log erstellen vor dem Löschen
|
||||
$contact->logs()->create([
|
||||
'action' => 'deleted',
|
||||
'description' => 'Kontakt entfernt - blockierte E-Mail-Domain (Buchungsplattform-Alias)',
|
||||
]);
|
||||
|
||||
$contact->delete();
|
||||
$stats['deleted']++;
|
||||
}
|
||||
}
|
||||
|
||||
$bar->advance();
|
||||
}
|
||||
|
||||
$bar->finish();
|
||||
$this->newLine(2);
|
||||
|
||||
// Statistiken ausgeben
|
||||
if ($dryRun) {
|
||||
$this->info('Dry-Run abgeschlossen!');
|
||||
$this->table(
|
||||
['Statistik', 'Anzahl'],
|
||||
[
|
||||
['Gefundene blockierte E-Mails', $stats['found']],
|
||||
]
|
||||
);
|
||||
|
||||
if ($stats['found'] > 0) {
|
||||
$this->newLine();
|
||||
$this->info('Führe den Befehl ohne --dry-run aus, um die Kontakte zu löschen:');
|
||||
$this->comment('php artisan newsletter:cleanup-blocked-emails');
|
||||
}
|
||||
} else {
|
||||
$this->info('Bereinigung abgeschlossen!');
|
||||
$this->table(
|
||||
['Statistik', 'Anzahl'],
|
||||
[
|
||||
['Gefundene blockierte E-Mails', $stats['found']],
|
||||
['Gelöschte Kontakte', $stats['deleted']],
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
254
app/Console/Commands/SyncNewsletterFerienwohnungen.php
Normal file
254
app/Console/Commands/SyncNewsletterFerienwohnungen.php
Normal file
|
|
@ -0,0 +1,254 @@
|
|||
<?php
|
||||
|
||||
namespace App\Console\Commands;
|
||||
|
||||
use App\Models\TravelUserBookingFewo;
|
||||
use App\Models\TravelUser;
|
||||
use App\Models\NewsletterContact;
|
||||
use Illuminate\Console\Command;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
|
||||
class SyncNewsletterFerienwohnungen extends Command
|
||||
{
|
||||
/**
|
||||
* The name and signature of the console command.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $signature = 'newsletter:sync-ferienwohnungen {--force : Force full sync}';
|
||||
|
||||
/**
|
||||
* The console description of the command.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $description = 'Synchronisiert Ferienwohnungs-Buchungen mit Newsletter-Kontakten';
|
||||
|
||||
/**
|
||||
* Execute the console command.
|
||||
*
|
||||
* @return int
|
||||
*/
|
||||
public function handle()
|
||||
{
|
||||
$this->info('Starte Synchronisation von Ferienwohnungs-Buchungen...');
|
||||
|
||||
$force = $this->option('force');
|
||||
|
||||
// Statistiken
|
||||
$stats = [
|
||||
'processed' => 0,
|
||||
'created' => 0,
|
||||
'updated' => 0,
|
||||
'skipped' => 0,
|
||||
'errors' => 0,
|
||||
];
|
||||
|
||||
// Hole alle Buchungen mit TravelUser und invoice_number
|
||||
$query = TravelUserBookingFewo::with(['travel_user'])
|
||||
->whereNotNull('travel_user_id')
|
||||
->whereNotNull('invoice_number')
|
||||
->where('invoice_number', '!=', '')
|
||||
// Nur wenn invoice_number eine reine Nummer ist (keine Storno etc.)
|
||||
->whereRaw('invoice_number REGEXP "^[0-9]+$"')
|
||||
->whereHas('travel_user', function ($q) {
|
||||
$q->whereNotNull('email')
|
||||
->where('email', '!=', '');
|
||||
})
|
||||
// Nur Buchungen, bei denen die Reise bereits beendet ist (to_date in der Vergangenheit)
|
||||
->whereNotNull('to_date')
|
||||
->where('to_date', '<', now());
|
||||
|
||||
if (!$force) {
|
||||
// Nur Buchungen der letzten 30 Tage (basierend auf Rückreisedatum) wenn nicht --force
|
||||
$query->where('to_date', '>=', now()->subDays(30));
|
||||
}
|
||||
|
||||
$bookings = $query->get();
|
||||
|
||||
$this->info("Verarbeite {$bookings->count()} Buchungen...");
|
||||
|
||||
$bar = $this->output->createProgressBar($bookings->count());
|
||||
$bar->start();
|
||||
|
||||
foreach ($bookings as $booking) {
|
||||
try {
|
||||
$stats['processed']++;
|
||||
|
||||
$travelUser = $booking->travel_user;
|
||||
|
||||
// Validiere E-Mail
|
||||
if (!$travelUser || !$travelUser->email || !filter_var($travelUser->email, FILTER_VALIDATE_EMAIL)) {
|
||||
$stats['skipped']++;
|
||||
$bar->advance();
|
||||
continue;
|
||||
}
|
||||
|
||||
// Filtere Alias/Proxy E-Mail-Adressen von Buchungsplattformen
|
||||
$blockedDomains = [
|
||||
'@guest.booking.com',
|
||||
'@messages.homeaway.com',
|
||||
'@fewo.check24.de',
|
||||
'@booking.com',
|
||||
'@homeaway.com',
|
||||
'@check24.de',
|
||||
'@partner.booking.com',
|
||||
];
|
||||
|
||||
$emailLower = strtolower($travelUser->email);
|
||||
$isBlockedEmail = false;
|
||||
foreach ($blockedDomains as $domain) {
|
||||
if (str_ends_with($emailLower, strtolower($domain))) {
|
||||
$isBlockedEmail = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if ($isBlockedEmail) {
|
||||
$stats['skipped']++;
|
||||
$bar->advance();
|
||||
continue;
|
||||
}
|
||||
|
||||
// Prüfe ob invoice_number wirklich eine reine Zahl ist
|
||||
if (!preg_match('/^[0-9]+$/', $booking->invoice_number)) {
|
||||
$stats['skipped']++;
|
||||
$bar->advance();
|
||||
continue;
|
||||
}
|
||||
|
||||
// Generiere Hash für Duplikat-Erkennung
|
||||
$syncHash = NewsletterContact::generateSyncHash(
|
||||
$travelUser->email,
|
||||
NewsletterContact::SOURCE_BOOKING_FERIENWOHNUNGEN
|
||||
);
|
||||
|
||||
// Suche oder erstelle Kontakt
|
||||
$contact = NewsletterContact::withTrashed()
|
||||
->where('email', strtolower(trim($travelUser->email)))
|
||||
->first();
|
||||
|
||||
$isNew = false;
|
||||
|
||||
if (!$contact) {
|
||||
// Neuer Kontakt
|
||||
$contact = new NewsletterContact();
|
||||
$isNew = true;
|
||||
$stats['created']++;
|
||||
} else {
|
||||
// Wenn gelöscht, wiederherstellen
|
||||
if ($contact->trashed()) {
|
||||
$contact->restore();
|
||||
}
|
||||
$stats['updated']++;
|
||||
}
|
||||
|
||||
// Aktualisiere Kontaktdaten
|
||||
$contact->email = strtolower(trim($travelUser->email));
|
||||
$contact->firstname = $travelUser->first_name ?: $contact->firstname;
|
||||
$contact->lastname = $travelUser->last_name ?: $contact->lastname;
|
||||
|
||||
// Setze Gruppe Ferienwohnungen
|
||||
$contact->group_ferienwohnungen = true;
|
||||
|
||||
// Source nur bei neuem Kontakt setzen (wenn noch nicht aus Kulturreisen)
|
||||
if ($isNew) {
|
||||
$contact->source = NewsletterContact::SOURCE_BOOKING_FERIENWOHNUNGEN;
|
||||
$contact->subscribed_at = $booking->booking_date ?
|
||||
\Carbon\Carbon::parse($booking->booking_date) :
|
||||
$booking->created_at;
|
||||
}
|
||||
|
||||
// Referenz zum TravelUser
|
||||
$contact->travel_user_id = $travelUser->id;
|
||||
|
||||
// Aktualisiere Buchungsstatistiken
|
||||
// Nur Buchungen mit invoice_number (reine Nummer) zählen
|
||||
$userBookings = TravelUserBookingFewo::where('travel_user_id', $travelUser->id)
|
||||
->whereNotNull('invoice_number')
|
||||
->where('invoice_number', '!=', '')
|
||||
->whereRaw('invoice_number REGEXP "^[0-9]+$"')
|
||||
->count();
|
||||
|
||||
$contact->total_bookings_ferienwohnungen = $userBookings;
|
||||
|
||||
// Letztes Buchungsdatum
|
||||
$lastBooking = TravelUserBookingFewo::where('travel_user_id', $travelUser->id)
|
||||
->whereNotNull('invoice_number')
|
||||
->where('invoice_number', '!=', '')
|
||||
->whereRaw('invoice_number REGEXP "^[0-9]+$"')
|
||||
->orderBy('booking_date', 'DESC')
|
||||
->first();
|
||||
|
||||
if ($lastBooking && $lastBooking->booking_date) {
|
||||
$lastBookingDate = \Carbon\Carbon::parse($lastBooking->booking_date);
|
||||
if (!$contact->last_booking_at || $lastBookingDate->gt($contact->last_booking_at)) {
|
||||
$contact->last_booking_at = $lastBookingDate;
|
||||
}
|
||||
}
|
||||
|
||||
// Letztes Reiseenddatum (to_date) - nur abgeschlossene Reisen
|
||||
$lastTravelEndBooking = TravelUserBookingFewo::where('travel_user_id', $travelUser->id)
|
||||
->whereNotNull('invoice_number')
|
||||
->where('invoice_number', '!=', '')
|
||||
->whereRaw('invoice_number REGEXP "^[0-9]+$"')
|
||||
->whereNotNull('to_date')
|
||||
->where('to_date', '<', now())
|
||||
->orderBy('to_date', 'DESC')
|
||||
->first();
|
||||
|
||||
if ($lastTravelEndBooking && $lastTravelEndBooking->to_date) {
|
||||
$lastTravelEndDate = \Carbon\Carbon::parse($lastTravelEndBooking->to_date);
|
||||
if (!$contact->last_travel_end_date || $lastTravelEndDate->gt($contact->last_travel_end_date)) {
|
||||
$contact->last_travel_end_date = $lastTravelEndDate;
|
||||
}
|
||||
}
|
||||
|
||||
// Status
|
||||
if ($isNew || $contact->status === NewsletterContact::STATUS_INACTIVE) {
|
||||
$contact->status = NewsletterContact::STATUS_ACTIVE;
|
||||
}
|
||||
|
||||
$contact->sync_hash = $syncHash;
|
||||
$contact->last_synced_at = now();
|
||||
|
||||
$contact->save();
|
||||
|
||||
// Log erstellen
|
||||
if ($isNew) {
|
||||
$contact->logs()->create([
|
||||
'action' => 'booking_added',
|
||||
'description' => 'Kontakt durch Ferienwohnungs-Buchung erstellt',
|
||||
'metadata' => [
|
||||
'booking_id' => $booking->id,
|
||||
'invoice_number' => $booking->invoice_number,
|
||||
],
|
||||
]);
|
||||
}
|
||||
} catch (\Exception $e) {
|
||||
$stats['errors']++;
|
||||
$this->error("Fehler bei Buchung {$booking->id}: " . $e->getMessage());
|
||||
}
|
||||
|
||||
$bar->advance();
|
||||
}
|
||||
|
||||
$bar->finish();
|
||||
$this->newLine(2);
|
||||
|
||||
// Statistiken ausgeben
|
||||
$this->info('Synchronisation abgeschlossen!');
|
||||
$this->table(
|
||||
['Statistik', 'Anzahl'],
|
||||
[
|
||||
['Verarbeitet', $stats['processed']],
|
||||
['Neu erstellt', $stats['created']],
|
||||
['Aktualisiert', $stats['updated']],
|
||||
['Übersprungen', $stats['skipped']],
|
||||
['Fehler', $stats['errors']],
|
||||
]
|
||||
);
|
||||
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
226
app/Console/Commands/SyncNewsletterKulturreisen.php
Normal file
226
app/Console/Commands/SyncNewsletterKulturreisen.php
Normal file
|
|
@ -0,0 +1,226 @@
|
|||
<?php
|
||||
|
||||
namespace App\Console\Commands;
|
||||
|
||||
use App\Models\Booking;
|
||||
use App\Models\Customer;
|
||||
use App\Models\NewsletterContact;
|
||||
use App\Models\Status;
|
||||
use Illuminate\Console\Command;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
|
||||
class SyncNewsletterKulturreisen extends Command
|
||||
{
|
||||
/**
|
||||
* The name and signature of the console command.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $signature = 'newsletter:sync-kulturreisen {--force : Force full sync}';
|
||||
|
||||
/**
|
||||
* The console description of the command.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $description = 'Synchronisiert Kulturreisen-Buchungen mit Newsletter-Kontakten';
|
||||
|
||||
/**
|
||||
* Execute the console command.
|
||||
*
|
||||
* @return int
|
||||
*/
|
||||
public function handle()
|
||||
{
|
||||
$this->info('Starte Synchronisation von Kulturreisen-Buchungen...');
|
||||
|
||||
$force = $this->option('force');
|
||||
|
||||
// Statistiken
|
||||
$stats = [
|
||||
'processed' => 0,
|
||||
'created' => 0,
|
||||
'updated' => 0,
|
||||
'skipped' => 0,
|
||||
'errors' => 0,
|
||||
];
|
||||
|
||||
// Hole alle Buchungen mit Kunden
|
||||
$query = Booking::with(['customer', 'lead.status'])
|
||||
->whereNotNull('customer_id')
|
||||
->whereHas('customer', function ($q) {
|
||||
$q->whereNotNull('email')
|
||||
->where('email', '!=', '');
|
||||
})
|
||||
// Nur Buchungen, bei denen die Reise bereits beendet ist (end_date in der Vergangenheit)
|
||||
->whereNotNull('end_date')
|
||||
->where('end_date', '<', now());
|
||||
|
||||
if (!$force) {
|
||||
// Nur Buchungen der letzten 30 Tage (basierend auf Rückreisedatum) wenn nicht --force
|
||||
$query->where('end_date', '>=', now()->subDays(30));
|
||||
}
|
||||
|
||||
$bookings = $query->get();
|
||||
|
||||
$this->info("Verarbeite {$bookings->count()} Buchungen...");
|
||||
|
||||
$bar = $this->output->createProgressBar($bookings->count());
|
||||
$bar->start();
|
||||
|
||||
foreach ($bookings as $booking) {
|
||||
try {
|
||||
$stats['processed']++;
|
||||
|
||||
$customer = $booking->customer;
|
||||
|
||||
// Validiere E-Mail
|
||||
if (!$customer || !$customer->email || !filter_var($customer->email, FILTER_VALIDATE_EMAIL)) {
|
||||
$stats['skipped']++;
|
||||
$bar->advance();
|
||||
continue;
|
||||
}
|
||||
|
||||
// Filtere Alias/Proxy E-Mail-Adressen von Buchungsplattformen
|
||||
$blockedDomains = [
|
||||
'@guest.booking.com',
|
||||
'@messages.homeaway.com',
|
||||
'@fewo.check24.de',
|
||||
'@booking.com',
|
||||
'@homeaway.com',
|
||||
'@check24.de',
|
||||
'@partner.booking.com',
|
||||
];
|
||||
|
||||
$emailLower = strtolower($customer->email);
|
||||
$isBlockedEmail = false;
|
||||
foreach ($blockedDomains as $domain) {
|
||||
if (str_ends_with($emailLower, strtolower($domain))) {
|
||||
$isBlockedEmail = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if ($isBlockedEmail) {
|
||||
$stats['skipped']++;
|
||||
$bar->advance();
|
||||
continue;
|
||||
}
|
||||
|
||||
// Generiere Hash für Duplikat-Erkennung
|
||||
$syncHash = NewsletterContact::generateSyncHash(
|
||||
$customer->email,
|
||||
NewsletterContact::SOURCE_BOOKING_KULTURREISEN
|
||||
);
|
||||
|
||||
// Suche oder erstelle Kontakt
|
||||
$contact = NewsletterContact::withTrashed()
|
||||
->where('email', strtolower(trim($customer->email)))
|
||||
->first();
|
||||
|
||||
$isNew = false;
|
||||
|
||||
if (!$contact) {
|
||||
// Neuer Kontakt
|
||||
$contact = new NewsletterContact();
|
||||
$isNew = true;
|
||||
$stats['created']++;
|
||||
} else {
|
||||
// Wenn gelöscht, wiederherstellen
|
||||
if ($contact->trashed()) {
|
||||
$contact->restore();
|
||||
}
|
||||
$stats['updated']++;
|
||||
}
|
||||
|
||||
// Aktualisiere Kontaktdaten
|
||||
$contact->email = strtolower(trim($customer->email));
|
||||
$contact->firstname = $customer->firstname ?: $contact->firstname;
|
||||
$contact->lastname = $customer->name ?: $contact->lastname;
|
||||
|
||||
// Setze Gruppe Kulturreisen
|
||||
$contact->group_kulturreisen = true;
|
||||
|
||||
// Source nur bei neuem Kontakt setzen
|
||||
if ($isNew) {
|
||||
$contact->source = NewsletterContact::SOURCE_BOOKING_KULTURREISEN;
|
||||
$contact->subscribed_at = $booking->booking_date ?: $booking->created_at;
|
||||
}
|
||||
|
||||
// Referenz zum Customer
|
||||
$contact->customer_id = $customer->id;
|
||||
|
||||
// Aktualisiere Buchungsstatistiken
|
||||
$customerBookings = Booking::where('customer_id', $customer->id)->count();
|
||||
$contact->total_bookings_kulturreisen = $customerBookings;
|
||||
|
||||
// Letztes Buchungsdatum
|
||||
$lastBooking = Booking::where('customer_id', $customer->id)
|
||||
->orderBy('booking_date', 'DESC')
|
||||
->first();
|
||||
|
||||
if ($lastBooking && $lastBooking->booking_date) {
|
||||
$contact->last_booking_at = $lastBooking->booking_date;
|
||||
}
|
||||
|
||||
// Letztes Reiseenddatum (end_date) - nur abgeschlossene Reisen
|
||||
$lastTravelEndBooking = Booking::where('customer_id', $customer->id)
|
||||
->whereNotNull('end_date')
|
||||
->where('end_date', '<', now())
|
||||
->orderBy('end_date', 'DESC')
|
||||
->first();
|
||||
|
||||
if ($lastTravelEndBooking && $lastTravelEndBooking->end_date) {
|
||||
if (!$contact->last_travel_end_date || $lastTravelEndBooking->end_date->gt($contact->last_travel_end_date)) {
|
||||
$contact->last_travel_end_date = $lastTravelEndBooking->end_date;
|
||||
}
|
||||
}
|
||||
|
||||
// Status
|
||||
if ($isNew || $contact->status === NewsletterContact::STATUS_INACTIVE) {
|
||||
$contact->status = NewsletterContact::STATUS_ACTIVE;
|
||||
}
|
||||
|
||||
$contact->sync_hash = $syncHash;
|
||||
$contact->last_synced_at = now();
|
||||
|
||||
$contact->save();
|
||||
|
||||
// Log erstellen
|
||||
if ($isNew) {
|
||||
$contact->logs()->create([
|
||||
'action' => 'booking_added',
|
||||
'description' => 'Kontakt durch Kulturreisen-Buchung erstellt',
|
||||
'metadata' => [
|
||||
'booking_id' => $booking->id,
|
||||
'lead_id' => $booking->lead_id,
|
||||
],
|
||||
]);
|
||||
}
|
||||
} catch (\Exception $e) {
|
||||
$stats['errors']++;
|
||||
$this->error("Fehler bei Buchung {$booking->id}: " . $e->getMessage());
|
||||
}
|
||||
|
||||
$bar->advance();
|
||||
}
|
||||
|
||||
$bar->finish();
|
||||
$this->newLine(2);
|
||||
|
||||
// Statistiken ausgeben
|
||||
$this->info('Synchronisation abgeschlossen!');
|
||||
$this->table(
|
||||
['Statistik', 'Anzahl'],
|
||||
[
|
||||
['Verarbeitet', $stats['processed']],
|
||||
['Neu erstellt', $stats['created']],
|
||||
['Aktualisiert', $stats['updated']],
|
||||
['Übersprungen', $stats['skipped']],
|
||||
['Fehler', $stats['errors']],
|
||||
]
|
||||
);
|
||||
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
18
app/Console/Commands/readme.md
Normal file
18
app/Console/Commands/readme.md
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
Newsletter-Synchronisation (wie gewohnt):
|
||||
|
||||
# Normale Synchronisation (letzte 30 Tage)
|
||||
|
||||
php artisan newsletter:sync-ferienwohnungenphp artisan newsletter:sync-kulturreisen
|
||||
|
||||
# Vollständige Synchronisation
|
||||
|
||||
php artisan newsletter:sync-ferienwohnungen --forcephp artisan newsletter:sync-kulturreisen --force
|
||||
#Bereinigung bestehender blockierter E-Mails:
|
||||
|
||||
# Erst testen (zeigt nur an, was gelöscht würde)
|
||||
|
||||
php artisan newsletter:cleanup-blocked-emails --dry-run
|
||||
|
||||
# Tatsächlich löschen
|
||||
|
||||
php artisan newsletter:cleanup-blocked-emails
|
||||
76
app/Exports/NewsletterExport.php
Normal file
76
app/Exports/NewsletterExport.php
Normal file
|
|
@ -0,0 +1,76 @@
|
|||
<?php
|
||||
|
||||
namespace App\Exports;
|
||||
|
||||
use App\Models\NewsletterContact;
|
||||
use Maatwebsite\Excel\Concerns\FromCollection;
|
||||
use Maatwebsite\Excel\Concerns\WithHeadings;
|
||||
use Maatwebsite\Excel\Concerns\WithMapping;
|
||||
use Maatwebsite\Excel\Concerns\ShouldAutoSize;
|
||||
|
||||
class NewsletterExport implements FromCollection, WithHeadings, WithMapping, ShouldAutoSize
|
||||
{
|
||||
protected $contacts;
|
||||
|
||||
public function __construct($contacts)
|
||||
{
|
||||
$this->contacts = $contacts;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return \Illuminate\Support\Collection
|
||||
*/
|
||||
public function collection()
|
||||
{
|
||||
return $this->contacts;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array
|
||||
*/
|
||||
public function headings(): array
|
||||
{
|
||||
return [
|
||||
'ID',
|
||||
'E-Mail',
|
||||
'Vorname',
|
||||
'Nachname',
|
||||
'Gruppe Kulturreisen',
|
||||
'Gruppe Ferienwohnungen',
|
||||
'Status',
|
||||
'Herkunft',
|
||||
'Buchungen Kulturreisen',
|
||||
'Buchungen Ferienwohnungen',
|
||||
'Letzte Buchung',
|
||||
'Letzte Reise',
|
||||
'Angemeldet am',
|
||||
'Abgemeldet am',
|
||||
'Erstellt am',
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @param NewsletterContact $contact
|
||||
* @return array
|
||||
*/
|
||||
public function map($contact): array
|
||||
{
|
||||
return [
|
||||
$contact->id,
|
||||
$contact->email,
|
||||
$contact->firstname,
|
||||
$contact->lastname,
|
||||
$contact->group_kulturreisen ? 'Ja' : 'Nein',
|
||||
$contact->group_ferienwohnungen ? 'Ja' : 'Nein',
|
||||
$contact->status_label,
|
||||
$contact->source_label,
|
||||
$contact->total_bookings_kulturreisen,
|
||||
$contact->total_bookings_ferienwohnungen,
|
||||
$contact->last_booking_at ? $contact->last_booking_at->format('d.m.Y') : '',
|
||||
$contact->last_travel_end_date ? $contact->last_travel_end_date->format('d.m.Y') : '',
|
||||
$contact->subscribed_at ? $contact->subscribed_at->format('d.m.Y H:i') : '',
|
||||
$contact->unsubscribed_at ? $contact->unsubscribed_at->format('d.m.Y H:i') : '',
|
||||
$contact->created_at->format('d.m.Y H:i'),
|
||||
];
|
||||
}
|
||||
}
|
||||
185
app/Http/Controllers/API/NavigationController.php
Normal file
185
app/Http/Controllers/API/NavigationController.php
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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'));
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
|
|
|||
165
app/Http/Controllers/NavigationTreeController.php
Normal file
165
app/Http/Controllers/NavigationTreeController.php
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
391
app/Http/Controllers/NewsletterController.php
Normal file
391
app/Http/Controllers/NewsletterController.php
Normal 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'),
|
||||
];
|
||||
}
|
||||
}
|
||||
326
app/Models/NewsletterContact.php
Normal file
326
app/Models/NewsletterContact.php
Normal file
|
|
@ -0,0 +1,326 @@
|
|||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Carbon\Carbon;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\SoftDeletes;
|
||||
use Illuminate\Database\Eloquent\Collection;
|
||||
|
||||
/**
|
||||
* Class NewsletterContact
|
||||
*
|
||||
* @property int $id
|
||||
* @property string $email
|
||||
* @property string|null $firstname
|
||||
* @property string|null $lastname
|
||||
* @property bool $group_kulturreisen
|
||||
* @property bool $group_ferienwohnungen
|
||||
* @property string $source
|
||||
* @property string $status
|
||||
* @property Carbon|null $subscribed_at
|
||||
* @property Carbon|null $unsubscribed_at
|
||||
* @property Carbon|null $last_booking_at
|
||||
* @property Carbon|null $last_travel_end_date
|
||||
* @property int $total_bookings_kulturreisen
|
||||
* @property int $total_bookings_ferienwohnungen
|
||||
* @property int|null $customer_id
|
||||
* @property int|null $travel_user_id
|
||||
* @property Carbon|null $last_synced_at
|
||||
* @property string|null $sync_hash
|
||||
* @property string|null $notes
|
||||
* @property Carbon $created_at
|
||||
* @property Carbon $updated_at
|
||||
* @property Carbon|null $deleted_at
|
||||
* @property-read Customer|null $customer
|
||||
* @property-read TravelUser|null $travel_user
|
||||
* @property-read Collection|NewsletterLog[] $logs
|
||||
* @package App\Models
|
||||
*/
|
||||
class NewsletterContact extends Model
|
||||
{
|
||||
use SoftDeletes;
|
||||
|
||||
protected $connection = 'mysql';
|
||||
|
||||
protected $table = 'newsletter_contacts';
|
||||
|
||||
protected $casts = [
|
||||
'group_kulturreisen' => 'boolean',
|
||||
'group_ferienwohnungen' => 'boolean',
|
||||
'subscribed_at' => 'datetime',
|
||||
'unsubscribed_at' => 'datetime',
|
||||
'last_booking_at' => 'datetime',
|
||||
'last_travel_end_date' => 'datetime',
|
||||
'last_synced_at' => 'datetime',
|
||||
'total_bookings_kulturreisen' => 'int',
|
||||
'total_bookings_ferienwohnungen' => 'int',
|
||||
'customer_id' => 'int',
|
||||
'travel_user_id' => 'int',
|
||||
];
|
||||
|
||||
protected $fillable = [
|
||||
'email',
|
||||
'firstname',
|
||||
'lastname',
|
||||
'group_kulturreisen',
|
||||
'group_ferienwohnungen',
|
||||
'source',
|
||||
'status',
|
||||
'subscribed_at',
|
||||
'unsubscribed_at',
|
||||
'last_booking_at',
|
||||
'last_travel_end_date',
|
||||
'total_bookings_kulturreisen',
|
||||
'total_bookings_ferienwohnungen',
|
||||
'customer_id',
|
||||
'travel_user_id',
|
||||
'last_synced_at',
|
||||
'sync_hash',
|
||||
'notes',
|
||||
];
|
||||
|
||||
// Konstanten für Source
|
||||
const SOURCE_BOOKING_KULTURREISEN = 'booking_kulturreisen';
|
||||
const SOURCE_BOOKING_FERIENWOHNUNGEN = 'booking_ferienwohnungen';
|
||||
const SOURCE_NEWSLETTER_SIGNUP = 'newsletter_signup';
|
||||
const SOURCE_MANUAL = 'manual';
|
||||
const SOURCE_IMPORT = 'import';
|
||||
|
||||
// Konstanten für Status
|
||||
const STATUS_ACTIVE = 'active';
|
||||
const STATUS_INACTIVE = 'inactive';
|
||||
const STATUS_UNSUBSCRIBED = 'unsubscribed';
|
||||
const STATUS_BOUNCED = 'bounced';
|
||||
|
||||
public static $sourceLabels = [
|
||||
self::SOURCE_BOOKING_KULTURREISEN => 'Buchung Kulturreisen',
|
||||
self::SOURCE_BOOKING_FERIENWOHNUNGEN => 'Buchung Ferienwohnungen',
|
||||
self::SOURCE_NEWSLETTER_SIGNUP => 'Newsletter-Anmeldung',
|
||||
self::SOURCE_MANUAL => 'Manuell',
|
||||
self::SOURCE_IMPORT => 'Import',
|
||||
];
|
||||
|
||||
public static $statusLabels = [
|
||||
self::STATUS_ACTIVE => 'Aktiv',
|
||||
self::STATUS_INACTIVE => 'Inaktiv',
|
||||
self::STATUS_UNSUBSCRIBED => 'Abgemeldet',
|
||||
self::STATUS_BOUNCED => 'Bounced',
|
||||
];
|
||||
|
||||
public static $statusColors = [
|
||||
self::STATUS_ACTIVE => 'success',
|
||||
self::STATUS_INACTIVE => 'secondary',
|
||||
self::STATUS_UNSUBSCRIBED => 'warning',
|
||||
self::STATUS_BOUNCED => 'danger',
|
||||
];
|
||||
|
||||
/**
|
||||
* Beziehung zum Customer (Kulturreisen)
|
||||
*/
|
||||
public function customer()
|
||||
{
|
||||
return $this->belongsTo(Customer::class, 'customer_id');
|
||||
}
|
||||
|
||||
/**
|
||||
* Beziehung zum TravelUser (Ferienwohnungen)
|
||||
*/
|
||||
public function travel_user()
|
||||
{
|
||||
return $this->belongsTo(TravelUser::class, 'travel_user_id');
|
||||
}
|
||||
|
||||
/**
|
||||
* Logs zu diesem Kontakt
|
||||
*/
|
||||
public function logs()
|
||||
{
|
||||
return $this->hasMany(NewsletterLog::class, 'newsletter_contact_id')->orderBy('created_at', 'DESC');
|
||||
}
|
||||
|
||||
/**
|
||||
* Vollständiger Name
|
||||
*/
|
||||
public function getFullNameAttribute()
|
||||
{
|
||||
return trim($this->firstname . ' ' . $this->lastname);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gruppenzugehörigkeit als Array
|
||||
*/
|
||||
public function getGroupsAttribute()
|
||||
{
|
||||
$groups = [];
|
||||
if ($this->group_kulturreisen) {
|
||||
$groups[] = 'Kulturreisen';
|
||||
}
|
||||
if ($this->group_ferienwohnungen) {
|
||||
$groups[] = 'Ferienwohnungen';
|
||||
}
|
||||
return $groups;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gruppenzugehörigkeit als String
|
||||
*/
|
||||
public function getGroupsStringAttribute()
|
||||
{
|
||||
return implode(', ', $this->groups);
|
||||
}
|
||||
|
||||
/**
|
||||
* Status-Label
|
||||
*/
|
||||
public function getStatusLabelAttribute()
|
||||
{
|
||||
return self::$statusLabels[$this->status] ?? $this->status;
|
||||
}
|
||||
|
||||
/**
|
||||
* Status-Color
|
||||
*/
|
||||
public function getStatusColorAttribute()
|
||||
{
|
||||
return self::$statusColors[$this->status] ?? 'secondary';
|
||||
}
|
||||
|
||||
/**
|
||||
* Source-Label
|
||||
*/
|
||||
public function getSourceLabelAttribute()
|
||||
{
|
||||
return self::$sourceLabels[$this->source] ?? $this->source;
|
||||
}
|
||||
|
||||
/**
|
||||
* Status-Badge HTML
|
||||
*/
|
||||
public function getStatusBadgeAttribute()
|
||||
{
|
||||
return '<span class="badge badge-' . $this->status_color . '">' . $this->status_label . '</span>';
|
||||
}
|
||||
|
||||
/**
|
||||
* Gesamtzahl Buchungen
|
||||
*/
|
||||
public function getTotalBookingsAttribute()
|
||||
{
|
||||
return $this->total_bookings_kulturreisen + $this->total_bookings_ferienwohnungen;
|
||||
}
|
||||
|
||||
/**
|
||||
* Ist Kontakt aktiv?
|
||||
*/
|
||||
public function isActive()
|
||||
{
|
||||
return $this->status === self::STATUS_ACTIVE;
|
||||
}
|
||||
|
||||
/**
|
||||
* Ist Kontakt abgemeldet?
|
||||
*/
|
||||
public function isUnsubscribed()
|
||||
{
|
||||
return $this->status === self::STATUS_UNSUBSCRIBED;
|
||||
}
|
||||
|
||||
/**
|
||||
* Hat Kontakt mindestens eine Buchung?
|
||||
*/
|
||||
public function hasBookings()
|
||||
{
|
||||
return $this->total_bookings > 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Kontakt abmelden
|
||||
*/
|
||||
public function unsubscribe($reason = null)
|
||||
{
|
||||
$this->status = self::STATUS_UNSUBSCRIBED;
|
||||
$this->unsubscribed_at = now();
|
||||
$this->save();
|
||||
|
||||
// Log erstellen
|
||||
$this->logs()->create([
|
||||
'action' => 'unsubscribed',
|
||||
'description' => $reason ?? 'Kontakt abgemeldet',
|
||||
]);
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Kontakt wieder aktivieren
|
||||
*/
|
||||
public function resubscribe()
|
||||
{
|
||||
$this->status = self::STATUS_ACTIVE;
|
||||
$this->unsubscribed_at = null;
|
||||
$this->save();
|
||||
|
||||
// Log erstellen
|
||||
$this->logs()->create([
|
||||
'action' => 'subscribed',
|
||||
'description' => 'Kontakt wieder aktiviert',
|
||||
]);
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Hash für Duplikat-Erkennung generieren
|
||||
*/
|
||||
public static function generateSyncHash($email, $source)
|
||||
{
|
||||
return md5(strtolower(trim($email)) . '_' . $source);
|
||||
}
|
||||
|
||||
/**
|
||||
* Scope: Nur aktive Kontakte
|
||||
*/
|
||||
public function scopeActive($query)
|
||||
{
|
||||
return $query->where('status', self::STATUS_ACTIVE);
|
||||
}
|
||||
|
||||
/**
|
||||
* Scope: Nur Kulturreisen
|
||||
*/
|
||||
public function scopeKulturreisen($query)
|
||||
{
|
||||
return $query->where('group_kulturreisen', true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Scope: Nur Ferienwohnungen
|
||||
*/
|
||||
public function scopeFerienwohnungen($query)
|
||||
{
|
||||
return $query->where('group_ferienwohnungen', true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Scope: Mit Buchungen
|
||||
*/
|
||||
public function scopeWithBookings($query)
|
||||
{
|
||||
return $query->where(function ($q) {
|
||||
$q->where('total_bookings_kulturreisen', '>', 0)
|
||||
->orWhere('total_bookings_ferienwohnungen', '>', 0);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Scope: Mehrfachbucher
|
||||
*/
|
||||
public function scopeMultipleBookers($query)
|
||||
{
|
||||
return $query->where(function ($q) {
|
||||
$q->where('total_bookings_kulturreisen', '>', 1)
|
||||
->orWhere('total_bookings_ferienwohnungen', '>', 1);
|
||||
});
|
||||
}
|
||||
}
|
||||
86
app/Models/NewsletterLog.php
Normal file
86
app/Models/NewsletterLog.php
Normal file
|
|
@ -0,0 +1,86 @@
|
|||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Carbon\Carbon;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
|
||||
/**
|
||||
* Class NewsletterLog
|
||||
*
|
||||
* @property int $id
|
||||
* @property int $newsletter_contact_id
|
||||
* @property string $action
|
||||
* @property string|null $description
|
||||
* @property array|null $metadata
|
||||
* @property int|null $user_id
|
||||
* @property Carbon $created_at
|
||||
* @property Carbon $updated_at
|
||||
* @property-read NewsletterContact $newsletter_contact
|
||||
* @property-read SfGuardUser|null $user
|
||||
* @package App\Models
|
||||
*/
|
||||
class NewsletterLog extends Model
|
||||
{
|
||||
protected $connection = 'mysql';
|
||||
|
||||
protected $table = 'newsletter_logs';
|
||||
|
||||
protected $casts = [
|
||||
'newsletter_contact_id' => 'int',
|
||||
'user_id' => 'int',
|
||||
'metadata' => 'array',
|
||||
];
|
||||
|
||||
protected $fillable = [
|
||||
'newsletter_contact_id',
|
||||
'action',
|
||||
'description',
|
||||
'metadata',
|
||||
'user_id',
|
||||
];
|
||||
|
||||
// Aktions-Konstanten
|
||||
const ACTION_SUBSCRIBED = 'subscribed';
|
||||
const ACTION_UNSUBSCRIBED = 'unsubscribed';
|
||||
const ACTION_BOOKING_ADDED = 'booking_added';
|
||||
const ACTION_STATUS_CHANGED = 'status_changed';
|
||||
const ACTION_GROUP_CHANGED = 'group_changed';
|
||||
const ACTION_EMAIL_SENT = 'email_sent';
|
||||
const ACTION_BOUNCED = 'bounced';
|
||||
|
||||
public static $actionLabels = [
|
||||
self::ACTION_SUBSCRIBED => 'Angemeldet',
|
||||
self::ACTION_UNSUBSCRIBED => 'Abgemeldet',
|
||||
self::ACTION_BOOKING_ADDED => 'Buchung hinzugefügt',
|
||||
self::ACTION_STATUS_CHANGED => 'Status geändert',
|
||||
self::ACTION_GROUP_CHANGED => 'Gruppe geändert',
|
||||
self::ACTION_EMAIL_SENT => 'E-Mail versendet',
|
||||
self::ACTION_BOUNCED => 'Bounced',
|
||||
];
|
||||
|
||||
/**
|
||||
* Beziehung zum Newsletter-Kontakt
|
||||
*/
|
||||
public function newsletter_contact()
|
||||
{
|
||||
return $this->belongsTo(NewsletterContact::class, 'newsletter_contact_id');
|
||||
}
|
||||
|
||||
/**
|
||||
* Beziehung zum Admin-User
|
||||
*/
|
||||
public function user()
|
||||
{
|
||||
return $this->belongsTo(SfGuardUser::class, 'user_id');
|
||||
}
|
||||
|
||||
/**
|
||||
* Action-Label
|
||||
*/
|
||||
public function getActionLabelAttribute()
|
||||
{
|
||||
return self::$actionLabels[$this->action] ?? $this->action;
|
||||
}
|
||||
}
|
||||
|
||||
681
app/Services/NavigationTreeService.php
Normal file
681
app/Services/NavigationTreeService.php
Normal file
|
|
@ -0,0 +1,681 @@
|
|||
<?php
|
||||
|
||||
namespace App\Services;
|
||||
|
||||
use App\Models\Page;
|
||||
use Illuminate\Support\Collection;
|
||||
use Illuminate\Support\Facades\Cache;
|
||||
|
||||
class NavigationTreeService
|
||||
{
|
||||
/**
|
||||
* Cache-Zeit in Minuten
|
||||
* @var int
|
||||
*/
|
||||
protected $cacheTime = 60;
|
||||
|
||||
/**
|
||||
* Ob Caching aktiviert ist
|
||||
* @var bool
|
||||
*/
|
||||
protected $cacheEnabled = true;
|
||||
|
||||
/**
|
||||
* Aktiviert oder deaktiviert Caching
|
||||
*
|
||||
* @param bool $enabled
|
||||
* @return $this
|
||||
*/
|
||||
public function setCacheEnabled(bool $enabled): self
|
||||
{
|
||||
$this->cacheEnabled = $enabled;
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Setzt die Cache-Zeit
|
||||
*
|
||||
* @param int $minutes
|
||||
* @return $this
|
||||
*/
|
||||
public function setCacheTime(int $minutes): self
|
||||
{
|
||||
$this->cacheTime = $minutes;
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Löscht den Navigation-Cache
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function clearCache(): void
|
||||
{
|
||||
Cache::forget('navigation_tree_full');
|
||||
Cache::forget('navigation_tree_active');
|
||||
Cache::forget('navigation_flat_full');
|
||||
Cache::forget('navigation_flat_active');
|
||||
|
||||
// Lösche auch alle Subtree-Caches
|
||||
$keys = Cache::get('navigation_subtree_keys', []);
|
||||
foreach ($keys as $key) {
|
||||
Cache::forget($key);
|
||||
}
|
||||
Cache::forget('navigation_subtree_keys');
|
||||
}
|
||||
/**
|
||||
* Gibt den kompletten Navigationsbaum zurück
|
||||
*
|
||||
* @param bool $onlyActive Nur aktive und sichtbare Seiten zurückgeben
|
||||
* @param bool $onlyShowInNavi Nur Seiten die in der Navigation angezeigt werden sollen
|
||||
* @return array
|
||||
*/
|
||||
public function getNavigationTree(bool $onlyActive = false, bool $onlyShowInNavi = false): array
|
||||
{
|
||||
$cacheKey = $onlyActive ? 'navigation_tree_active' : 'navigation_tree_full';
|
||||
|
||||
if ($this->cacheEnabled) {
|
||||
return Cache::remember($cacheKey, $this->cacheTime, function () use ($onlyActive, $onlyShowInNavi) {
|
||||
return $this->buildNavigationTree($onlyActive, $onlyShowInNavi);
|
||||
});
|
||||
}
|
||||
|
||||
return $this->buildNavigationTree($onlyActive, $onlyShowInNavi);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gibt den Navigationsbaum wie im Frontend zurück (nur Länderseiten mit Children)
|
||||
*
|
||||
* @param bool $includeHidden Auch ausgeblendete Pages anzeigen
|
||||
* @return array
|
||||
*/
|
||||
public function getFrontendNavigationTree(bool $includeHidden = false): array
|
||||
{
|
||||
$cacheKey = 'navigation_tree_frontend_' . ($includeHidden ? 'with_hidden' : 'visible');
|
||||
|
||||
if ($this->cacheEnabled) {
|
||||
return Cache::remember($cacheKey, $this->cacheTime, function () use ($includeHidden) {
|
||||
return $this->buildFrontendNavigationTree($includeHidden);
|
||||
});
|
||||
}
|
||||
|
||||
return $this->buildFrontendNavigationTree($includeHidden);
|
||||
}
|
||||
|
||||
/**
|
||||
* Baut den Frontend-Navigationsbaum auf
|
||||
*
|
||||
* @param bool $includeHidden
|
||||
* @return array
|
||||
*/
|
||||
protected function buildFrontendNavigationTree(bool $includeHidden = false): array
|
||||
{
|
||||
$tree = [];
|
||||
|
||||
// 1. Länderseiten (Hauptnavigation)
|
||||
$countryPages = $this->getCountryPages($includeHidden);
|
||||
foreach ($countryPages as $page) {
|
||||
$node = $this->buildFrontendNode($page, $includeHidden);
|
||||
if ($node) {
|
||||
$node['section'] = 'Länder-Navigation';
|
||||
$tree[] = $node;
|
||||
}
|
||||
}
|
||||
|
||||
// 2. Ferienwohnungen (USEDOM)
|
||||
$fewoPages = $this->getFewoPages($includeHidden);
|
||||
if (!empty($fewoPages)) {
|
||||
$tree[] = [
|
||||
'is_section_separator' => true,
|
||||
'title' => 'USEDOM Ferienwohnungen',
|
||||
'icon' => 'isv-fewo',
|
||||
'section' => 'Ferienwohnungen'
|
||||
];
|
||||
foreach ($fewoPages as $page) {
|
||||
$node = $this->buildFrontendNode($page, $includeHidden);
|
||||
if ($node) {
|
||||
$node['section'] = 'Ferienwohnungen';
|
||||
$tree[] = $node;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 3. Weitere wichtige Seiten (Mehr-Menü)
|
||||
$morePages = $this->getMoreMenuPages($includeHidden);
|
||||
if (!empty($morePages)) {
|
||||
$tree[] = [
|
||||
'is_section_separator' => true,
|
||||
'title' => 'Weitere Seiten (Mehr-Menü)',
|
||||
'icon' => 'fa fa-ellipsis-v',
|
||||
'section' => 'Mehr'
|
||||
];
|
||||
foreach ($morePages as $page) {
|
||||
$node = $this->buildFrontendNode($page, $includeHidden);
|
||||
if ($node) {
|
||||
$node['section'] = 'Mehr';
|
||||
$tree[] = $node;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $tree;
|
||||
}
|
||||
|
||||
/**
|
||||
* Hole alle Länderseiten
|
||||
*
|
||||
* @param bool $includeHidden
|
||||
* @return \Illuminate\Database\Eloquent\Collection
|
||||
*/
|
||||
protected function getCountryPages(bool $includeHidden = false)
|
||||
{
|
||||
$query = Page::whereNull('parent_id')
|
||||
->whereNotNull('country_id')
|
||||
->orderBy('order')
|
||||
->orderBy('title');
|
||||
|
||||
if (!$includeHidden) {
|
||||
$query->where('show_in_navi', 1);
|
||||
$query->where('status', 1);
|
||||
}
|
||||
|
||||
return $query->get();
|
||||
}
|
||||
|
||||
/**
|
||||
* Hole Ferienwohnungs-Übersichtsseite mit Children
|
||||
*
|
||||
* @param bool $includeHidden
|
||||
* @return array
|
||||
*/
|
||||
protected function getFewoPages(bool $includeHidden = false)
|
||||
{
|
||||
// Suche die Hauptseite "Ferienwohnungen"
|
||||
$query = Page::where('slug', 'ferienwohnungen')
|
||||
->orWhere('real_url_path', '/ferienwohnungen');
|
||||
|
||||
if (!$includeHidden) {
|
||||
$query->where('status', 1);
|
||||
}
|
||||
|
||||
$fewoMainPage = $query->first();
|
||||
|
||||
if (!$fewoMainPage) {
|
||||
return [];
|
||||
}
|
||||
|
||||
// Nur die Hauptseite zurückgeben, Children werden über buildFrontendNode geladen
|
||||
return [$fewoMainPage];
|
||||
}
|
||||
|
||||
/**
|
||||
* Hole Seiten für "Mehr"-Menü
|
||||
*
|
||||
* @param bool $includeHidden
|
||||
* @return array
|
||||
*/
|
||||
protected function getMoreMenuPages(bool $includeHidden = false)
|
||||
{
|
||||
// Typische Slugs/Pfade aus dem Mehr-Menü mit ihren möglichen Children
|
||||
$morePages = [
|
||||
'ueber-uns' => true, // Könnte Children haben
|
||||
'reiseversicherung' => false, // Keine Children
|
||||
'reisefuehrer' => true, // Könnte Children haben
|
||||
'reisemagazin' => true, // Könnte Children haben
|
||||
'reisenews' => true // Könnte Children haben
|
||||
];
|
||||
|
||||
$pages = [];
|
||||
|
||||
foreach ($morePages as $slug => $hasChildren) {
|
||||
$query = Page::where('slug', $slug)
|
||||
->orWhere('real_url_path', '/' . $slug);
|
||||
|
||||
if (!$includeHidden) {
|
||||
$query->where('status', 1);
|
||||
}
|
||||
|
||||
$page = $query->first();
|
||||
if ($page) {
|
||||
$pages[] = $page;
|
||||
}
|
||||
}
|
||||
|
||||
return $pages;
|
||||
}
|
||||
|
||||
/**
|
||||
* Baut einen Frontend-Node auf (mit Children gruppiert nach beforeTitle)
|
||||
*
|
||||
* @param Page $page
|
||||
* @param bool $includeHidden
|
||||
* @param bool $loadChildren Soll Children geladen werden?
|
||||
* @return array|null
|
||||
*/
|
||||
protected function buildFrontendNode(Page $page, bool $includeHidden = false, bool $loadChildren = true): ?array
|
||||
{
|
||||
$node = $this->buildNodeData($page, true);
|
||||
|
||||
if (!$loadChildren) {
|
||||
$node['children'] = [];
|
||||
$node['has_children'] = false;
|
||||
return $node;
|
||||
}
|
||||
|
||||
// Hole Children
|
||||
$query = Page::where('parent_id', $page->id)
|
||||
->orderBy('order')
|
||||
->orderBy('title');
|
||||
|
||||
if (!$includeHidden) {
|
||||
$query->where('show_in_navi', 1);
|
||||
$query->where('status', 1);
|
||||
}
|
||||
|
||||
$children = $query->get();
|
||||
|
||||
// Gruppiere Children nach beforeTitle
|
||||
$groupedChildren = [
|
||||
'main' => [],
|
||||
'infos' => []
|
||||
];
|
||||
|
||||
foreach ($children as $child) {
|
||||
$childNode = $this->buildNodeData($child, true);
|
||||
|
||||
if ($child->before_title === 'Infos') {
|
||||
$groupedChildren['infos'][] = $childNode;
|
||||
} else {
|
||||
$groupedChildren['main'][] = $childNode;
|
||||
}
|
||||
}
|
||||
|
||||
// Baue finale Children-Liste mit Gruppierung
|
||||
$finalChildren = [];
|
||||
|
||||
// Erst Haupt-Children
|
||||
foreach ($groupedChildren['main'] as $childNode) {
|
||||
$finalChildren[] = $childNode;
|
||||
}
|
||||
|
||||
// Dann Info-Children mit Separator
|
||||
if (!empty($groupedChildren['infos'])) {
|
||||
$finalChildren[] = [
|
||||
'is_separator' => true,
|
||||
'title' => 'Infos',
|
||||
'icon' => 'fa fa-info-circle'
|
||||
];
|
||||
foreach ($groupedChildren['infos'] as $childNode) {
|
||||
$finalChildren[] = $childNode;
|
||||
}
|
||||
}
|
||||
|
||||
$node['children'] = $finalChildren;
|
||||
$node['has_children'] = count($finalChildren) > 0;
|
||||
|
||||
return $node;
|
||||
}
|
||||
|
||||
/**
|
||||
* Baut den Navigationsbaum auf (ohne Caching)
|
||||
*
|
||||
* @param bool $onlyActive
|
||||
* @param bool $onlyShowInNavi
|
||||
* @return array
|
||||
*/
|
||||
protected function buildNavigationTree(bool $onlyActive = false, bool $onlyShowInNavi = false): array
|
||||
{
|
||||
// Hole alle Root-Seiten (ohne Parent)
|
||||
$query = Page::whereNull('parent_id')
|
||||
->orderBy('order')
|
||||
->orderBy('title');
|
||||
|
||||
if ($onlyActive) {
|
||||
$query->where('status', 1);
|
||||
}
|
||||
|
||||
if ($onlyShowInNavi) {
|
||||
$query->where('show_in_navi', 1);
|
||||
}
|
||||
|
||||
$rootPages = $query->get();
|
||||
|
||||
$tree = [];
|
||||
foreach ($rootPages as $page) {
|
||||
$tree[] = $this->buildNode($page, $onlyActive, $onlyShowInNavi);
|
||||
}
|
||||
|
||||
return $tree;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gibt einen Teilbaum zurück, beginnend mit einer bestimmten Page
|
||||
*
|
||||
* @param int $rootId Die ID der Root-Page
|
||||
* @param bool $onlyActive Nur aktive Seiten zurückgeben
|
||||
* @param bool $onlyShowInNavi Nur Seiten die in der Navigation angezeigt werden sollen
|
||||
* @return array|null
|
||||
*/
|
||||
public function getNavigationSubTree(int $rootId, bool $onlyActive = false, bool $onlyShowInNavi = false): ?array
|
||||
{
|
||||
$cacheKey = "navigation_subtree_{$rootId}_" . ($onlyActive ? 'active' : 'full');
|
||||
|
||||
if ($this->cacheEnabled) {
|
||||
// Speichere Cache-Key für späteres Löschen
|
||||
$keys = Cache::get('navigation_subtree_keys', []);
|
||||
if (!in_array($cacheKey, $keys)) {
|
||||
$keys[] = $cacheKey;
|
||||
Cache::put('navigation_subtree_keys', $keys, $this->cacheTime);
|
||||
}
|
||||
|
||||
return Cache::remember($cacheKey, $this->cacheTime, function () use ($rootId, $onlyActive, $onlyShowInNavi) {
|
||||
return $this->buildSubTree($rootId, $onlyActive, $onlyShowInNavi);
|
||||
});
|
||||
}
|
||||
|
||||
return $this->buildSubTree($rootId, $onlyActive, $onlyShowInNavi);
|
||||
}
|
||||
|
||||
/**
|
||||
* Baut einen Teilbaum auf (ohne Caching)
|
||||
*
|
||||
* @param int $rootId
|
||||
* @param bool $onlyActive
|
||||
* @param bool $onlyShowInNavi
|
||||
* @return array|null
|
||||
*/
|
||||
protected function buildSubTree(int $rootId, bool $onlyActive = false, bool $onlyShowInNavi = false): ?array
|
||||
{
|
||||
$page = Page::find($rootId);
|
||||
|
||||
if (!$page) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return $this->buildNode($page, $onlyActive, $onlyShowInNavi);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gibt eine flache Liste aller Navigationspunkte zurück
|
||||
*
|
||||
* @param bool $onlyActive Nur aktive Seiten zurückgeben
|
||||
* @param bool $onlyShowInNavi Nur Seiten die in der Navigation angezeigt werden sollen
|
||||
* @return array
|
||||
*/
|
||||
public function getFlatNavigationList(bool $onlyActive = false, bool $onlyShowInNavi = false): array
|
||||
{
|
||||
$cacheKey = $onlyActive ? 'navigation_flat_active' : 'navigation_flat_full';
|
||||
|
||||
if ($this->cacheEnabled) {
|
||||
return Cache::remember($cacheKey, $this->cacheTime, function () use ($onlyActive, $onlyShowInNavi) {
|
||||
return $this->buildFlatList($onlyActive, $onlyShowInNavi);
|
||||
});
|
||||
}
|
||||
|
||||
return $this->buildFlatList($onlyActive, $onlyShowInNavi);
|
||||
}
|
||||
|
||||
/**
|
||||
* Baut eine flache Liste auf (ohne Caching)
|
||||
*
|
||||
* @param bool $onlyActive
|
||||
* @param bool $onlyShowInNavi
|
||||
* @return array
|
||||
*/
|
||||
protected function buildFlatList(bool $onlyActive = false, bool $onlyShowInNavi = false): array
|
||||
{
|
||||
$query = Page::orderBy('order')
|
||||
->orderBy('title');
|
||||
|
||||
if ($onlyActive) {
|
||||
$query->where('status', 1);
|
||||
}
|
||||
|
||||
if ($onlyShowInNavi) {
|
||||
$query->where('show_in_navi', 1);
|
||||
}
|
||||
|
||||
$pages = $query->get();
|
||||
|
||||
$list = [];
|
||||
foreach ($pages as $page) {
|
||||
$list[] = $this->buildNodeData($page, false);
|
||||
}
|
||||
|
||||
return $list;
|
||||
}
|
||||
|
||||
/**
|
||||
* Baut einen einzelnen Knoten mit allen Kindern rekursiv auf
|
||||
*
|
||||
* @param Page $page
|
||||
* @param bool $onlyActive
|
||||
* @param bool $onlyShowInNavi
|
||||
* @return array
|
||||
*/
|
||||
protected function buildNode(Page $page, bool $onlyActive = false, bool $onlyShowInNavi = false): array
|
||||
{
|
||||
$node = $this->buildNodeData($page, true);
|
||||
|
||||
// Hole alle Child-Seiten
|
||||
$query = Page::where('parent_id', $page->id)
|
||||
->orderBy('order')
|
||||
->orderBy('title');
|
||||
|
||||
if ($onlyActive) {
|
||||
$query->where('status', 1);
|
||||
}
|
||||
|
||||
if ($onlyShowInNavi) {
|
||||
$query->where('show_in_navi', 1);
|
||||
}
|
||||
|
||||
$children = $query->get();
|
||||
|
||||
$childNodes = [];
|
||||
foreach ($children as $child) {
|
||||
$childNodes[] = $this->buildNode($child, $onlyActive, $onlyShowInNavi);
|
||||
}
|
||||
|
||||
$node['children'] = $childNodes;
|
||||
$node['has_children'] = count($childNodes) > 0;
|
||||
|
||||
return $node;
|
||||
}
|
||||
|
||||
/**
|
||||
* Erstellt die Datenstruktur für einen einzelnen Navigationspunkt
|
||||
*
|
||||
* @param Page $page
|
||||
* @param bool $includeRelations Beziehungen laden (TravelProgram, etc.)
|
||||
* @return array
|
||||
*/
|
||||
protected function buildNodeData(Page $page, bool $includeRelations = true): array
|
||||
{
|
||||
$data = [
|
||||
'id' => $page->id,
|
||||
'title' => $page->title,
|
||||
'title_short' => $page->title_short,
|
||||
'before_title' => $page->before_title,
|
||||
'slug' => $page->slug,
|
||||
'real_url_path' => $page->real_url_path,
|
||||
'url' => $page->real_url_path ?: $this->buildUrlPath($page),
|
||||
'status' => $page->status,
|
||||
'show_in_navi' => $page->show_in_navi,
|
||||
'order' => $page->order,
|
||||
'template' => $page->template,
|
||||
'lvl' => $page->lvl,
|
||||
'parent_id' => $page->parent_id,
|
||||
|
||||
// SEO-Informationen
|
||||
'pagetitle' => $page->pagetitle,
|
||||
'description' => $page->description,
|
||||
'keywords' => $page->keywords,
|
||||
'canonical_url' => $page->canonical_url,
|
||||
|
||||
// Tree-Informationen
|
||||
'lft' => $page->lft,
|
||||
'rgt' => $page->rgt,
|
||||
'tree_root' => $page->tree_root,
|
||||
|
||||
// Box-Informationen (für Karten/Boxen in der Navigation)
|
||||
'box_body' => $page->box_body,
|
||||
'box_image_url' => $page->box_image_url,
|
||||
'box_star' => $page->box_star,
|
||||
'box_discount' => $page->box_discount,
|
||||
|
||||
// Zusätzliche Flags
|
||||
'is_travel_program' => !is_null($page->travel_program),
|
||||
'is_fewo_lodging' => !is_null($page->fewo_lodging),
|
||||
'is_country_page' => !is_null($page->country_id),
|
||||
];
|
||||
|
||||
if ($includeRelations) {
|
||||
// Lade Beziehungen wenn benötigt
|
||||
if ($page->travel_program) {
|
||||
$travelProgram = $page->travel_program_content;
|
||||
if ($travelProgram) {
|
||||
$data['travel_program'] = [
|
||||
'id' => $travelProgram->id,
|
||||
'title' => $travelProgram->title,
|
||||
'subtitle' => $travelProgram->subtitle,
|
||||
'status' => $travelProgram->status,
|
||||
'position' => $travelProgram->position,
|
||||
'duration' => $travelProgram->duration,
|
||||
'price_from' => $travelProgram->price_from,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
if ($page->fewo_lodging) {
|
||||
$fewoLodging = $page->fewo_lodging();
|
||||
if ($fewoLodging) {
|
||||
$fewo = $fewoLodging->first();
|
||||
if ($fewo) {
|
||||
$data['fewo_lodging'] = [
|
||||
'id' => $fewo->id,
|
||||
'name' => $fewo->name,
|
||||
'status' => $fewo->status,
|
||||
];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if ($page->country_id) {
|
||||
$country = $page->travel_country;
|
||||
if ($country) {
|
||||
$data['country'] = [
|
||||
'id' => $country->id,
|
||||
'name' => $country->name,
|
||||
'title' => $country->title,
|
||||
'slug' => $country->slug,
|
||||
];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// CMS Settings (JSON)
|
||||
if ($page->cms_settings) {
|
||||
try {
|
||||
$data['cms_settings'] = json_decode($page->cms_settings, true);
|
||||
} catch (\Exception $e) {
|
||||
$data['cms_settings'] = null;
|
||||
}
|
||||
}
|
||||
|
||||
return $data;
|
||||
}
|
||||
|
||||
/**
|
||||
* Baut den URL-Pfad für eine Page rekursiv auf (Fallback wenn real_url_path nicht gesetzt)
|
||||
*
|
||||
* @param Page $page
|
||||
* @return string
|
||||
*/
|
||||
protected function buildUrlPath(Page $page): string
|
||||
{
|
||||
$slugs = [];
|
||||
$current = $page;
|
||||
|
||||
// Sammle alle Slugs von unten nach oben
|
||||
while ($current) {
|
||||
if ($current->slug) {
|
||||
array_unshift($slugs, $current->slug);
|
||||
}
|
||||
$current = $current->parent_page;
|
||||
}
|
||||
|
||||
return '/' . implode('/', $slugs);
|
||||
}
|
||||
|
||||
/**
|
||||
* Zählt die Anzahl der Knoten im Baum
|
||||
*
|
||||
* @param array $tree
|
||||
* @return int
|
||||
*/
|
||||
public function countNodes(array $tree): int
|
||||
{
|
||||
$count = 0;
|
||||
foreach ($tree as $node) {
|
||||
$count++; // Zähle den aktuellen Knoten
|
||||
if (isset($node['children']) && is_array($node['children'])) {
|
||||
$count += $this->countNodes($node['children']);
|
||||
}
|
||||
}
|
||||
return $count;
|
||||
}
|
||||
|
||||
/**
|
||||
* Findet einen Knoten anhand seiner ID im Baum
|
||||
*
|
||||
* @param array $tree
|
||||
* @param int $nodeId
|
||||
* @return array|null
|
||||
*/
|
||||
public function findNodeById(array $tree, int $nodeId): ?array
|
||||
{
|
||||
foreach ($tree as $node) {
|
||||
if ($node['id'] === $nodeId) {
|
||||
return $node;
|
||||
}
|
||||
if (isset($node['children']) && is_array($node['children'])) {
|
||||
$found = $this->findNodeById($node['children'], $nodeId);
|
||||
if ($found) {
|
||||
return $found;
|
||||
}
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gibt den Breadcrumb-Pfad für eine bestimmte Page zurück
|
||||
*
|
||||
* @param int $pageId
|
||||
* @return array
|
||||
*/
|
||||
public function getBreadcrumb(int $pageId): array
|
||||
{
|
||||
$page = Page::find($pageId);
|
||||
if (!$page) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$breadcrumb = [];
|
||||
$current = $page;
|
||||
|
||||
while ($current) {
|
||||
array_unshift($breadcrumb, [
|
||||
'id' => $current->id,
|
||||
'title' => $current->title,
|
||||
'title_short' => $current->title_short,
|
||||
'url' => $current->real_url_path ?: $this->buildUrlPath($current),
|
||||
'slug' => $current->slug,
|
||||
]);
|
||||
$current = $current->parent_page;
|
||||
}
|
||||
|
||||
return $breadcrumb;
|
||||
}
|
||||
}
|
||||
|
|
@ -1,12 +1,14 @@
|
|||
<?php
|
||||
|
||||
namespace App\Services;
|
||||
|
||||
class Util
|
||||
{
|
||||
|
||||
public static function isTestSystem($dev = false){
|
||||
if(\Config::get('app.domain_tld') === 'test'){
|
||||
if($dev && config('app.debug') !== true){
|
||||
public static function isTestSystem($dev = false)
|
||||
{
|
||||
if (\Config::get('app.domain_tld') === 'test') {
|
||||
if ($dev && config('app.debug') !== true) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
|
|
@ -14,119 +16,165 @@ class Util
|
|||
return false;
|
||||
}
|
||||
|
||||
public static function formatDate(){
|
||||
if(\App::getLocale() == "en"){
|
||||
public static function formatDate()
|
||||
{
|
||||
if (\App::getLocale() == "en") {
|
||||
return 'yyyy-mm-dd';
|
||||
}
|
||||
return 'dd.mm.yyyy';
|
||||
}
|
||||
|
||||
public static function formatDateDB(){
|
||||
if(\App::getLocale() == "en"){
|
||||
public static function formatDateDB()
|
||||
{
|
||||
if (\App::getLocale() == "en") {
|
||||
return 'Y-m-d';
|
||||
}
|
||||
return 'd.m.Y';
|
||||
}
|
||||
|
||||
public static function formatDateTimeDB(){
|
||||
if(\App::getLocale() == "en"){
|
||||
public static function formatDateTimeDB()
|
||||
{
|
||||
if (\App::getLocale() == "en") {
|
||||
return 'Y-m-d - H:i';
|
||||
}
|
||||
return 'd.m.Y - H:i';
|
||||
}
|
||||
public static function _format_text($text, $opt = 'html'){
|
||||
public static function _format_text($text, $opt = 'html')
|
||||
{
|
||||
|
||||
if($opt === 'html'){
|
||||
if ($opt === 'html') {
|
||||
return html_entity_decode(nl2br($text));
|
||||
}
|
||||
return $text;
|
||||
}
|
||||
|
||||
public static function _format_date($date, $to = 'date'){
|
||||
if($to === 'datetime'){
|
||||
public static function _format_date($date, $to = 'date')
|
||||
{
|
||||
if ($to === 'datetime') {
|
||||
return \Carbon::parse($date)->format(\Util::formatDateTimeDB());
|
||||
}
|
||||
//date
|
||||
return \Carbon::parse($date)->format(\Util::formatDateDB());
|
||||
}
|
||||
|
||||
public static function _reformat_date($date, $to = 'date'){
|
||||
if($to === 'datetime'){
|
||||
public static function _reformat_date($date, $to = 'date')
|
||||
{
|
||||
if ($to === 'datetime') {
|
||||
return \Carbon::parse($date)->format('Y-m-d - H:i');
|
||||
}
|
||||
return \Carbon::parse($date)->format('Y-m-d');
|
||||
}
|
||||
|
||||
public static function _format_number($value){
|
||||
public static function _format_number($value)
|
||||
{
|
||||
return preg_replace("/[^0-9,]/", "", $value);
|
||||
|
||||
}
|
||||
|
||||
public static function _number_format($value, $dec=2){
|
||||
public static function _number_format($value, $dec = 2)
|
||||
{
|
||||
return number_format(($value), $dec, ',', '.');
|
||||
|
||||
}
|
||||
|
||||
public static function _first_replace($value, $search='re:', $replace=''){
|
||||
public static function _first_replace($value, $search = 're:', $replace = '')
|
||||
{
|
||||
do {
|
||||
$before = strlen($value);
|
||||
$value = trim(preg_replace('/^'.$search.'/i', $replace, $value));
|
||||
|
||||
}while($before !== strlen($value));
|
||||
$value = trim(preg_replace('/^' . $search . '/i', $replace, $value));
|
||||
} while ($before !== strlen($value));
|
||||
return $value;
|
||||
|
||||
}
|
||||
public static function _explodeLines($value = false){
|
||||
if($value){
|
||||
return explode('#', str_replace(array("\r\n", "\r", "\n"),"#", $value));
|
||||
public static function _explodeLines($value = false)
|
||||
{
|
||||
if ($value) {
|
||||
return explode('#', str_replace(array("\r\n", "\r", "\n"), "#", $value));
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
public static function _implodeLines($value, $glue=PHP_EOL){
|
||||
if(is_array($value)){
|
||||
public static function _implodeLines($value, $glue = PHP_EOL)
|
||||
{
|
||||
if (is_array($value)) {
|
||||
return implode($glue, $value);
|
||||
}
|
||||
return $value;
|
||||
}
|
||||
public static function _clean_float($value){
|
||||
public static function _clean_float($value)
|
||||
{
|
||||
|
||||
$groups = explode(".", preg_replace("/[^0-9.-]/", "", str_replace(',', '.', $value)));
|
||||
$lastGroup = 0;
|
||||
if(count($groups) > 1){
|
||||
if (count($groups) > 1) {
|
||||
$lastGroup = array_pop($groups);
|
||||
}
|
||||
$number = implode('', $groups);
|
||||
return (strlen($lastGroup) < 3) ? floatval($number.'.'.$lastGroup) : floatval($number.$lastGroup);
|
||||
return (strlen($lastGroup) < 3) ? floatval($number . '.' . $lastGroup) : floatval($number . $lastGroup);
|
||||
}
|
||||
|
||||
|
||||
public static function sanitize($string, $force_lowercase = true, $anal = false, $substr = false)
|
||||
{
|
||||
$strip = array("~", "`", "!", "@", "#", "$", "%", "^", "&", "*", "(", ")", "_", "=", "+", "[", "{", "]",
|
||||
"}", "\\", "|", ";", ":", "\"", "'", "‘", "’", "“", "”", "–", "—",
|
||||
"—", "–", ",", "<", ".", ">", "/", "?");
|
||||
$strip = array(
|
||||
"~",
|
||||
"`",
|
||||
"!",
|
||||
"@",
|
||||
"#",
|
||||
"$",
|
||||
"%",
|
||||
"^",
|
||||
"&",
|
||||
"*",
|
||||
"(",
|
||||
")",
|
||||
"_",
|
||||
"=",
|
||||
"+",
|
||||
"[",
|
||||
"{",
|
||||
"]",
|
||||
"}",
|
||||
"\\",
|
||||
"|",
|
||||
";",
|
||||
":",
|
||||
"\"",
|
||||
"'",
|
||||
"‘",
|
||||
"’",
|
||||
"“",
|
||||
"”",
|
||||
"–",
|
||||
"—",
|
||||
"—",
|
||||
"–",
|
||||
",",
|
||||
"<",
|
||||
".",
|
||||
">",
|
||||
"/",
|
||||
"?"
|
||||
);
|
||||
$clean = trim(str_replace($strip, "", strip_tags($string)));
|
||||
$clean = preg_replace('/\s+/', "_", $clean);
|
||||
$clean = ($anal) ? preg_replace("/[^a-zA-Z0-9]/", "", $clean) : $clean ;
|
||||
|
||||
if($substr){
|
||||
$clean = (strlen($clean) > 33) ? substr($clean,0,33) : $clean;
|
||||
$clean = ($anal) ? preg_replace("/[^a-zA-Z0-9]/", "", $clean) : $clean;
|
||||
|
||||
if ($substr) {
|
||||
$clean = (strlen($clean) > 33) ? substr($clean, 0, 33) : $clean;
|
||||
}
|
||||
return ($force_lowercase) ?
|
||||
(function_exists('mb_strtolower')) ?
|
||||
mb_strtolower($clean, 'UTF-8') :
|
||||
strtolower($clean) :
|
||||
mb_strtolower($clean, 'UTF-8') :
|
||||
strtolower($clean) :
|
||||
$clean;
|
||||
}
|
||||
|
||||
public static function replacePlaceholders($search, $replace){
|
||||
public static function replacePlaceholders($search, $replace)
|
||||
{
|
||||
preg_match_all("/\{{(.+?)\}}/", $search, $matches);
|
||||
if (isset($matches[1]) && count($matches[1]) > 0){
|
||||
if (isset($matches[1]) && count($matches[1]) > 0) {
|
||||
foreach ($matches[1] as $key => $value) {
|
||||
$kvalue = trim($value);
|
||||
if (array_key_exists($kvalue, $replace)){
|
||||
if (array_key_exists($kvalue, $replace)) {
|
||||
$search = preg_replace("/\{\{$value\}\}/", $replace[$kvalue], $search);
|
||||
}
|
||||
}
|
||||
|
|
@ -158,7 +206,7 @@ class Util
|
|||
|
||||
|
||||
|
||||
// $html = preg_replace("/(</?)div/", "$1p", $html);
|
||||
// $html = preg_replace("/(</?)div/", "$1p", $html);
|
||||
|
||||
$html = str_replace('<p> </p>', '', $html);
|
||||
$html = str_replace('<p></p>', '', $html);
|
||||
|
|
@ -176,7 +224,7 @@ class Util
|
|||
@$dom->loadHTML(mb_convert_encoding($html, 'HTML-ENTITIES', 'UTF-8'));
|
||||
|
||||
$removeStyleTags = ['ul', 'li', 'h1', 'h2', 'br'];
|
||||
foreach ($removeStyleTags as $removeStyleTag){
|
||||
foreach ($removeStyleTags as $removeStyleTag) {
|
||||
$elements = $dom->getElementsByTagName($removeStyleTag);
|
||||
foreach ($elements as $element) {
|
||||
$element->removeAttribute('style');
|
||||
|
|
@ -184,17 +232,16 @@ class Util
|
|||
}
|
||||
|
||||
$removeFullTags = ['span', 'a'];
|
||||
foreach ($removeFullTags as $removeFullTag){
|
||||
foreach ($removeFullTags as $removeFullTag) {
|
||||
$domElemsToRemove = [];
|
||||
$elements = $dom->getElementsByTagName($removeFullTag);
|
||||
foreach ($elements as $element) {
|
||||
$domElemsToRemove[] = $element;
|
||||
|
||||
}
|
||||
foreach ($domElemsToRemove as $domElem) {
|
||||
if($removeFullTag == 'span' && strpos($domElem->getAttribute('style'), 'font-weight: 700') !== false){
|
||||
$new_node = $dom->createTextNode("<strong>".$domElem->nodeValue."</strong>");
|
||||
}else{
|
||||
if ($removeFullTag == 'span' && strpos($domElem->getAttribute('style'), 'font-weight: 700') !== false) {
|
||||
$new_node = $dom->createTextNode("<strong>" . $domElem->nodeValue . "</strong>");
|
||||
} else {
|
||||
$new_node = $dom->createTextNode($domElem->nodeValue);
|
||||
}
|
||||
$domElem->parentNode->replaceChild($new_node, $domElem);
|
||||
|
|
@ -235,28 +282,28 @@ class Util
|
|||
$element->parentNode->replaceChild($new_node, $element);
|
||||
} */
|
||||
|
||||
$new_node = $dom->createTextNode($element->nodeValue . "</p>");
|
||||
$p = $dom->createElement('p', $element->nodeValue);
|
||||
$div = $dom->createElement('div');
|
||||
// $new_node = $dom->createElement('div', $new_node);
|
||||
$div->setAttribute('class', 'mediaInfo');
|
||||
$div->appendChild($p);
|
||||
|
||||
// dump($element);
|
||||
// die();
|
||||
//
|
||||
$element->parentNode->replaceChild($div, $element);
|
||||
$new_node = $dom->createTextNode($element->nodeValue . "</p>");
|
||||
$p = $dom->createElement('p', $element->nodeValue);
|
||||
$div = $dom->createElement('div');
|
||||
// $new_node = $dom->createElement('div', $new_node);
|
||||
$div->setAttribute('class', 'mediaInfo');
|
||||
$div->appendChild($p);
|
||||
|
||||
// dump($element);
|
||||
// die();
|
||||
//
|
||||
$element->parentNode->replaceChild($div, $element);
|
||||
}
|
||||
|
||||
$html = $dom->saveHTML();
|
||||
return $html;
|
||||
}
|
||||
|
||||
public static function prepareCollapseValues(){
|
||||
if(\Session::has('collapse_shows')){
|
||||
public static function prepareCollapseValues()
|
||||
{
|
||||
if (\Session::has('collapse_shows')) {
|
||||
$collapse_shows = \Session::get('collapse_shows');
|
||||
if(strpos($collapse_shows, ',')){
|
||||
if (strpos($collapse_shows, ',')) {
|
||||
$collapse_shows = explode(',', $collapse_shows);
|
||||
return json_encode($collapse_shows);
|
||||
}
|
||||
|
|
@ -266,32 +313,35 @@ class Util
|
|||
return json_encode([0 => 'non']);
|
||||
}
|
||||
|
||||
public static function convertArrayWindowsCharset($values){
|
||||
foreach ($values as $key=>$string){
|
||||
public static function convertArrayWindowsCharset($values)
|
||||
{
|
||||
foreach ($values as $key => $string) {
|
||||
$values[$key] = self::convertStringWindowsCharset($string);
|
||||
}
|
||||
return $values;
|
||||
}
|
||||
|
||||
public static function convertStringWindowsCharset($value) {
|
||||
public static function convertStringWindowsCharset($value)
|
||||
{
|
||||
|
||||
$charset = mb_detect_encoding($value, "UTF-8, ISO-8859-1, ISO-8859-15", true);
|
||||
return mb_convert_encoding($value, "Windows-1252", $charset);
|
||||
}
|
||||
|
||||
|
||||
public static function getMimeFromHeader($http_response_header){
|
||||
public static function getMimeFromHeader($http_response_header)
|
||||
{
|
||||
$pattern = "/^content-type\s*:\s*(.*)$/i";
|
||||
if (($header = array_values(preg_grep($pattern, $http_response_header))) &&
|
||||
(preg_match($pattern, $header[0], $match) !== false))
|
||||
{
|
||||
(preg_match($pattern, $header[0], $match) !== false)
|
||||
) {
|
||||
return $match[1];
|
||||
|
||||
}
|
||||
return "";
|
||||
}
|
||||
|
||||
public static function getExtensionFromMime($mine){
|
||||
public static function getExtensionFromMime($mine)
|
||||
{
|
||||
$mime_types = [
|
||||
'application/pdf' => 'pdf',
|
||||
'image/png' => 'png',
|
||||
|
|
@ -304,15 +354,16 @@ class Util
|
|||
return isset($mime_types[$mine]) ? $mime_types[$mine] : "";
|
||||
}
|
||||
|
||||
public static function getURLasContent($url, $base=false){
|
||||
$arrContextOptions=array(
|
||||
"ssl"=>array(
|
||||
"verify_peer"=>false,
|
||||
"verify_peer_name"=>false,
|
||||
public static function getURLasContent($url, $base = false)
|
||||
{
|
||||
$arrContextOptions = array(
|
||||
"ssl" => array(
|
||||
"verify_peer" => false,
|
||||
"verify_peer_name" => false,
|
||||
),
|
||||
);
|
||||
$content = file_get_contents($url, false, stream_context_create($arrContextOptions));
|
||||
if($base){
|
||||
if ($base) {
|
||||
$type = pathinfo($url, PATHINFO_EXTENSION);
|
||||
$base64Data = base64_encode($content);
|
||||
return 'data:image/' . $type . ';base64,' . $base64Data;
|
||||
|
|
@ -332,6 +383,4 @@ class Util
|
|||
return $size;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue