23-01-2026
84
.devcontainer/devcontainer.json
Normal file
|
|
@ -0,0 +1,84 @@
|
|||
{
|
||||
"name": "STERN TOURS (Dev Container)",
|
||||
// 1. DIES IST DER WICHTIGSTE TEIL:
|
||||
// Wir verwenden Docker Compose für alle Services
|
||||
"dockerComposeFile": [
|
||||
"../docker-compose.yml"
|
||||
],
|
||||
"service": "laravel.test",
|
||||
// 3. WIR DEFINIEREN DEN ARBEITSBEREICH:
|
||||
// Das ist der Pfad, in dem Ihr Code *innerhalb* des Containers liegt.
|
||||
"workspaceFolder": "/var/www/html",
|
||||
// 4. WIR LEGEN DEN BENUTZER FEST:
|
||||
// Laravel Sail führt Befehle standardmäßig als 'sail'-Benutzer aus, um Berechtigungsprobleme zu vermeiden.
|
||||
"remoteUser": "sail",
|
||||
// 5. ZUSÄTZLICHE ENTWICKLER-TOOLS (FEATURES):
|
||||
// Features werden über postCreateCommand installiert um Kompatibilitätsprobleme zu vermeiden
|
||||
"features": {},
|
||||
// 6. BEFEHLE NACH DEM ERSTELLEN:
|
||||
// Installiert nur die Tools die ohne Root-Rechte funktionieren
|
||||
//"postCreateCommand": "composer install --no-interaction --prefer-dist --optimize-autoloader",
|
||||
// 7. EDITOR-ANPASSUNGEN (Optional, aber sehr empfohlen):
|
||||
"customizations": {
|
||||
"vscode": {
|
||||
"extensions": [
|
||||
"bmewburn.vscode-intelephense-client",
|
||||
"onecentlin.laravel-blade",
|
||||
"shufo.vscode-blade-formatter",
|
||||
"bradlc.vscode-tailwindcss"
|
||||
]
|
||||
}
|
||||
},
|
||||
// 8. ZU STARTENDE DIENSTE:
|
||||
// Legt fest, welche Dienste aus der docker-compose.yml gestartet werden sollen.
|
||||
"runServices": [
|
||||
"laravel.test",
|
||||
"mysql",
|
||||
"mysql-stern",
|
||||
"redis",
|
||||
"mailpit"
|
||||
],
|
||||
// 9. ZUSÄTZLICHE KONFIGURATION:
|
||||
// Umgebungsvariablen für den DevContainer
|
||||
"containerEnv": {
|
||||
"WWWUSER": "501",
|
||||
"WWWGROUP": "20",
|
||||
"LARAVEL_SAIL": "1"
|
||||
},
|
||||
// 10. MOUNT-KONFIGURATION:
|
||||
// Stellt sicher, dass der Code korrekt gemountet wird
|
||||
"mounts": [
|
||||
"source=${localWorkspaceFolder},target=/var/www/html,type=bind,consistency=cached"
|
||||
],
|
||||
// 11. FORWARD PORTS:
|
||||
// Ports die automatisch weitergeleitet werden sollen
|
||||
"forwardPorts": [
|
||||
33064,
|
||||
33065,
|
||||
6379,
|
||||
1030,
|
||||
8030
|
||||
],
|
||||
"portsAttributes": {
|
||||
"33064": {
|
||||
"label": "MySQL",
|
||||
"onAutoForward": "silent"
|
||||
},
|
||||
"33065": {
|
||||
"label": "MySQL Stern",
|
||||
"onAutoForward": "silent"
|
||||
},
|
||||
"6379": {
|
||||
"label": "Redis",
|
||||
"onAutoForward": "silent"
|
||||
},
|
||||
"1030": {
|
||||
"label": "Mailpit SMTP",
|
||||
"onAutoForward": "silent"
|
||||
},
|
||||
"8030": {
|
||||
"label": "Mailpit Dashboard",
|
||||
"onAutoForward": "notify"
|
||||
}
|
||||
}
|
||||
}
|
||||
455
INIT.md
Normal file
|
|
@ -0,0 +1,455 @@
|
|||
# Sterntours Laravel Projekt - Initialisierung & Setup
|
||||
|
||||
Diese Dokumentation beschreibt die Initialisierung und Einrichtung des Sterntours Laravel-Projekts.
|
||||
|
||||
## 📋 Inhaltsverzeichnis
|
||||
|
||||
1. [Voraussetzungen](#voraussetzungen)
|
||||
2. [Schnellstart](#schnellstart)
|
||||
3. [Manuelle Installation](#manuelle-installation)
|
||||
4. [Projekt-Struktur](#projekt-struktur)
|
||||
5. [Umgebungskonfiguration](#umgebungskonfiguration)
|
||||
6. [Datenbanken](#datenbanken)
|
||||
7. [Domains & Routing](#domains--routing)
|
||||
8. [Häufige Befehle](#häufige-befehle)
|
||||
9. [Fehlerbehebung](#fehlerbehebung)
|
||||
|
||||
---
|
||||
|
||||
## Voraussetzungen
|
||||
|
||||
Bevor du das Projekt initialisierst, stelle sicher, dass folgende Software installiert ist:
|
||||
|
||||
- **Docker Desktop** (Version 20.10+)
|
||||
- [Download für macOS](https://www.docker.com/products/docker-desktop)
|
||||
- [Download für Windows](https://www.docker.com/products/docker-desktop)
|
||||
- [Download für Linux](https://docs.docker.com/engine/install/)
|
||||
|
||||
- **Composer** (Version 2.0+)
|
||||
- [Installation Guide](https://getcomposer.org/download/)
|
||||
|
||||
- **Node.js & NPM** (Version 16+ empfohlen)
|
||||
- [Download](https://nodejs.org/)
|
||||
|
||||
- **Git** (für Versionskontrolle)
|
||||
- [Download](https://git-scm.com/)
|
||||
|
||||
---
|
||||
|
||||
## Schnellstart
|
||||
|
||||
Das Projekt kann mit einem einzigen Befehl initialisiert werden:
|
||||
|
||||
```bash
|
||||
bash init.sh
|
||||
```
|
||||
|
||||
Das Script führt automatisch alle notwendigen Schritte aus:
|
||||
|
||||
1. ✓ Prüft Docker-Installation
|
||||
2. ✓ Erstellt `.env` Datei
|
||||
3. ✓ Installiert Composer-Dependencies
|
||||
4. ✓ Installiert NPM-Dependencies
|
||||
5. ✓ Generiert Application Key
|
||||
6. ✓ Erstellt Docker-Netzwerke
|
||||
7. ✓ Startet Docker-Container
|
||||
8. ✓ Führt Datenbank-Migrationen aus
|
||||
9. ✓ Erstellt Storage-Links
|
||||
10. ✓ Optimiert die Application
|
||||
|
||||
---
|
||||
|
||||
## Manuelle Installation
|
||||
|
||||
Falls du das Projekt manuell einrichten möchtest:
|
||||
|
||||
### 1. Repository klonen
|
||||
|
||||
```bash
|
||||
git clone <repository-url> sterntours
|
||||
cd sterntours
|
||||
```
|
||||
|
||||
### 2. Umgebungsvariablen konfigurieren
|
||||
|
||||
```bash
|
||||
cp .env.example .env
|
||||
```
|
||||
|
||||
Bearbeite die `.env` Datei nach Bedarf (siehe [Umgebungskonfiguration](#umgebungskonfiguration)).
|
||||
|
||||
### 3. Dependencies installieren
|
||||
|
||||
```bash
|
||||
# Composer Dependencies
|
||||
composer install
|
||||
|
||||
# NPM Dependencies
|
||||
npm install
|
||||
```
|
||||
|
||||
### 4. Application Key generieren
|
||||
|
||||
```bash
|
||||
php artisan key:generate
|
||||
```
|
||||
|
||||
### 5. Docker Proxy-Netzwerk erstellen
|
||||
|
||||
```bash
|
||||
docker network create proxy
|
||||
```
|
||||
|
||||
### 6. Docker Container starten (Laravel Sail)
|
||||
|
||||
```bash
|
||||
./vendor/bin/sail up -d
|
||||
```
|
||||
|
||||
### 7. Datenbank migrieren
|
||||
|
||||
```bash
|
||||
./vendor/bin/sail artisan migrate
|
||||
```
|
||||
|
||||
### 8. Storage Links erstellen
|
||||
|
||||
```bash
|
||||
./vendor/bin/sail artisan storage:link
|
||||
```
|
||||
|
||||
### 9. Cache optimieren
|
||||
|
||||
```bash
|
||||
./vendor/bin/sail artisan config:cache
|
||||
./vendor/bin/sail artisan route:cache
|
||||
./vendor/bin/sail artisan view:cache
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Projekt-Struktur
|
||||
|
||||
```
|
||||
sterntours/
|
||||
├── app/ # Application Code
|
||||
│ ├── Console/ # Artisan Commands
|
||||
│ ├── Http/ # Controllers, Middleware
|
||||
│ ├── Models/ # Eloquent Models
|
||||
│ ├── Services/ # Business Logic
|
||||
│ └── Repositories/ # Data Access Layer
|
||||
├── config/ # Konfigurationsdateien
|
||||
├── database/ # Migrations, Seeds, Factories
|
||||
├── public/ # Öffentliche Assets
|
||||
├── resources/ # Views, Assets (Sass, JS)
|
||||
├── routes/ # Route Definitionen
|
||||
├── storage/ # Logs, Cache, Uploads
|
||||
├── tests/ # Unit & Feature Tests
|
||||
├── docker-compose.yml # Docker Setup
|
||||
├── init.sh # Initialisierungsskript
|
||||
└── INIT.md # Diese Datei
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Umgebungskonfiguration
|
||||
|
||||
### Wichtige .env Variablen
|
||||
|
||||
#### Haupt-Datenbank (CRM)
|
||||
```env
|
||||
DB_CONNECTION=mysql
|
||||
DB_HOST=mysql
|
||||
DB_PORT=3306
|
||||
DB_DATABASE=stern_crm
|
||||
DB_USERNAME=sail
|
||||
DB_PASSWORD=password
|
||||
```
|
||||
|
||||
#### Stern-Datenbank (Legacy)
|
||||
```env
|
||||
DB_CONNECTION_STERN=mysql
|
||||
DB_HOST_STERN=mysql-stern
|
||||
DB_PORT_STERN=3306
|
||||
DB_DATABASE_STERN=stern_db
|
||||
DB_USERNAME_STERN=sail
|
||||
DB_PASSWORD_STERN=password
|
||||
```
|
||||
|
||||
#### Mail-Konfiguration (Mailpit)
|
||||
```env
|
||||
MAIL_MAILER=smtp
|
||||
MAIL_HOST=mailpit
|
||||
MAIL_PORT=1025
|
||||
MAIL_ENCRYPTION=null
|
||||
```
|
||||
|
||||
#### Redis Cache
|
||||
```env
|
||||
REDIS_HOST=redis
|
||||
REDIS_PASSWORD=null
|
||||
REDIS_PORT=6379
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Datenbanken
|
||||
|
||||
Das Projekt verwendet **zwei separate MySQL-Datenbanken**:
|
||||
|
||||
### 1. stern_crm (Haupt-CRM)
|
||||
- **Port:** 33064
|
||||
- **Verwendung:** Haupt-Application, Kundenverwaltung
|
||||
- **Host in Container:** mysql
|
||||
|
||||
### 2. stern_db (Legacy Datenbank)
|
||||
- **Port:** 33065
|
||||
- **Verwendung:** Alte Stern-Tours Daten
|
||||
- **Host in Container:** mysql-stern
|
||||
|
||||
### Datenbankzugriff von außen
|
||||
|
||||
```bash
|
||||
# CRM Datenbank
|
||||
mysql -h 127.0.0.1 -P 33064 -u sail -p stern_crm
|
||||
|
||||
# Stern Datenbank
|
||||
mysql -h 127.0.0.1 -P 33065 -u sail -p stern_db
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Domains & Routing
|
||||
|
||||
Das Projekt verwendet **Traefik** als Reverse Proxy und ist über folgende Domains erreichbar:
|
||||
|
||||
| Domain | Zweck | Port |
|
||||
|--------|-------|------|
|
||||
| `https://mein.sterntours.test` | Haupt-Application | 443 |
|
||||
| `https://sterntours.test` | Alternative URL | 443 |
|
||||
| `https://assets.sterntours.test` | Vite Dev-Server | 5173 |
|
||||
| `https://sterntours-mail.test` | Mailpit Dashboard | 8025 |
|
||||
|
||||
### Hosts-Datei konfigurieren
|
||||
|
||||
Füge folgende Einträge zu deiner `/etc/hosts` (Linux/Mac) oder `C:\Windows\System32\drivers\etc\hosts` (Windows) hinzu:
|
||||
|
||||
```
|
||||
127.0.0.1 mein.sterntours.test
|
||||
127.0.0.1 sterntours.test
|
||||
127.0.0.1 assets.sterntours.test
|
||||
127.0.0.1 sterntours-mail.test
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Häufige Befehle
|
||||
|
||||
### Docker Container
|
||||
|
||||
```bash
|
||||
# Container starten
|
||||
./vendor/bin/sail up -d
|
||||
|
||||
# Container stoppen
|
||||
./vendor/bin/sail down
|
||||
|
||||
# Container neu bauen
|
||||
./vendor/bin/sail build --no-cache
|
||||
|
||||
# Logs anzeigen
|
||||
./vendor/bin/sail logs -f
|
||||
|
||||
# Spezifischen Service anzeigen
|
||||
./vendor/bin/sail logs mysql -f
|
||||
```
|
||||
|
||||
### Artisan Befehle
|
||||
|
||||
```bash
|
||||
# Migrationen ausführen
|
||||
./vendor/bin/sail artisan migrate
|
||||
|
||||
# Migrationen zurücksetzen
|
||||
./vendor/bin/sail artisan migrate:rollback
|
||||
|
||||
# Seeds ausführen
|
||||
./vendor/bin/sail artisan db:seed
|
||||
|
||||
# Cache leeren
|
||||
./vendor/bin/sail artisan cache:clear
|
||||
./vendor/bin/sail artisan config:clear
|
||||
./vendor/bin/sail artisan route:clear
|
||||
./vendor/bin/sail artisan view:clear
|
||||
|
||||
# Cache optimieren
|
||||
./vendor/bin/sail artisan config:cache
|
||||
./vendor/bin/sail artisan route:cache
|
||||
./vendor/bin/sail artisan view:cache
|
||||
```
|
||||
|
||||
### Composer
|
||||
|
||||
```bash
|
||||
# Packages installieren
|
||||
./vendor/bin/sail composer install
|
||||
|
||||
# Package hinzufügen
|
||||
./vendor/bin/sail composer require <package>
|
||||
|
||||
# Package entfernen
|
||||
./vendor/bin/sail composer remove <package>
|
||||
|
||||
# Autoload aktualisieren
|
||||
./vendor/bin/sail composer dump-autoload
|
||||
```
|
||||
|
||||
### NPM / Assets
|
||||
|
||||
```bash
|
||||
# Dependencies installieren
|
||||
npm install
|
||||
|
||||
# Development Build
|
||||
npm run dev
|
||||
|
||||
# Production Build
|
||||
npm run prod
|
||||
|
||||
# Watch Mode
|
||||
npm run watch
|
||||
```
|
||||
|
||||
### Tests
|
||||
|
||||
```bash
|
||||
# Alle Tests ausführen
|
||||
./vendor/bin/sail test
|
||||
|
||||
# Spezifische Test-Datei
|
||||
./vendor/bin/sail test tests/Feature/ExampleTest.php
|
||||
|
||||
# Mit Coverage
|
||||
./vendor/bin/sail test --coverage
|
||||
```
|
||||
|
||||
### Shell / SSH
|
||||
|
||||
```bash
|
||||
# Shell im Container öffnen
|
||||
./vendor/bin/sail shell
|
||||
|
||||
# Root Shell
|
||||
./vendor/bin/sail root-shell
|
||||
|
||||
# MySQL Shell
|
||||
./vendor/bin/sail mysql
|
||||
|
||||
# Redis CLI
|
||||
./vendor/bin/sail redis
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Fehlerbehebung
|
||||
|
||||
### Problem: "Docker is not running"
|
||||
|
||||
**Lösung:** Starte Docker Desktop und warte, bis es vollständig gestartet ist.
|
||||
|
||||
```bash
|
||||
# macOS/Linux
|
||||
sudo systemctl start docker
|
||||
|
||||
# Windows
|
||||
Starte Docker Desktop über das Startmenü
|
||||
```
|
||||
|
||||
### Problem: "Port already in use"
|
||||
|
||||
**Lösung:** Prüfe, welche Ports belegt sind und ändere sie in der `.env` Datei:
|
||||
|
||||
```bash
|
||||
# Ports prüfen
|
||||
lsof -i :33064
|
||||
lsof -i :33065
|
||||
|
||||
# In .env ändern
|
||||
FORWARD_DB_PORT=33064
|
||||
FORWARD_DB_PORT_STERN=33065
|
||||
```
|
||||
|
||||
### Problem: "Network proxy not found"
|
||||
|
||||
**Lösung:** Erstelle das Traefik Proxy-Netzwerk manuell:
|
||||
|
||||
```bash
|
||||
docker network create proxy
|
||||
```
|
||||
|
||||
### Problem: "Permission denied" beim init.sh
|
||||
|
||||
**Lösung:** Mache das Script ausführbar:
|
||||
|
||||
```bash
|
||||
chmod +x init.sh
|
||||
./init.sh
|
||||
```
|
||||
|
||||
### Problem: "Class not found"
|
||||
|
||||
**Lösung:** Regeneriere Composer Autoload:
|
||||
|
||||
```bash
|
||||
./vendor/bin/sail composer dump-autoload
|
||||
./vendor/bin/sail artisan clear-compiled
|
||||
```
|
||||
|
||||
### Problem: "Migration failed"
|
||||
|
||||
**Lösung:** Prüfe Datenbankverbindung und setze zurück:
|
||||
|
||||
```bash
|
||||
# Verbindung testen
|
||||
./vendor/bin/sail artisan tinker
|
||||
>>> DB::connection()->getPdo();
|
||||
|
||||
# Migrationen zurücksetzen
|
||||
./vendor/bin/sail artisan migrate:fresh
|
||||
```
|
||||
|
||||
### Problem: "Vite/Assets nicht geladen"
|
||||
|
||||
**Lösung:** Starte den Dev-Server:
|
||||
|
||||
```bash
|
||||
npm run dev
|
||||
# oder
|
||||
npm run watch
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Support & Kontakt
|
||||
|
||||
Bei Fragen oder Problemen:
|
||||
|
||||
1. Prüfe die [Fehlerbehebung](#fehlerbehebung)
|
||||
2. Prüfe die [Laravel Dokumentation](https://laravel.com/docs)
|
||||
3. Prüfe die [Laravel Sail Dokumentation](https://laravel.com/docs/sail)
|
||||
|
||||
---
|
||||
|
||||
## Changelog
|
||||
|
||||
### Version 1.0.0 (2025-11-07)
|
||||
- ✓ Initiales Setup-Script erstellt
|
||||
- ✓ Dokumentation erstellt
|
||||
- ✓ Docker Compose Konfiguration
|
||||
- ✓ Dual-Datenbank Setup
|
||||
- ✓ Traefik Routing konfiguriert
|
||||
|
||||
---
|
||||
|
||||
**Viel Erfolg mit dem Projekt! 🚀**
|
||||
|
||||
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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,7 +1,10 @@
|
|||
{
|
||||
"name": "laravel/laravel",
|
||||
"description": "The Laravel Framework.",
|
||||
"keywords": ["framework", "laravel"],
|
||||
"keywords": [
|
||||
"framework",
|
||||
"laravel"
|
||||
],
|
||||
"license": "MIT",
|
||||
"type": "project",
|
||||
"repositories": [
|
||||
|
|
@ -14,7 +17,7 @@
|
|||
}
|
||||
],
|
||||
"require": {
|
||||
"php": "^8.0|^8.2",
|
||||
"php": "^8.0|^8.2|^8.3",
|
||||
"bacon/bacon-qr-code": "^3.0",
|
||||
"barryvdh/laravel-dompdf": "*",
|
||||
"cviebrock/eloquent-sluggable": "*",
|
||||
|
|
@ -48,7 +51,7 @@
|
|||
"nunomaduro/collision": "^5.0",
|
||||
"phpunit/phpunit": "^9.3.3",
|
||||
"barryvdh/laravel-debugbar": "*",
|
||||
"barryvdh/laravel-ide-helper": "*"
|
||||
"barryvdh/laravel-ide-helper": "*"
|
||||
},
|
||||
"autoload": {
|
||||
"files": [
|
||||
|
|
@ -69,8 +72,7 @@
|
|||
},
|
||||
"extra": {
|
||||
"laravel": {
|
||||
"dont-discover": [
|
||||
]
|
||||
"dont-discover": []
|
||||
}
|
||||
},
|
||||
"scripts": {
|
||||
|
|
@ -99,4 +101,4 @@
|
|||
},
|
||||
"minimum-stability": "dev",
|
||||
"prefer-stable": true
|
||||
}
|
||||
}
|
||||
|
|
@ -1,74 +1,77 @@
|
|||
<?php
|
||||
|
||||
return [
|
||||
'groups' =>[
|
||||
'groups' => [
|
||||
0 => [
|
||||
'my-dat' => ['name' => 'Ihre Daten' , 'color' => 'client'],
|
||||
],
|
||||
'my-dat' => ['name' => 'Ihre Daten', 'color' => 'client'],
|
||||
],
|
||||
1 => [
|
||||
'crm' => ['name' => 'ADMIN CRM ' , 'color' => 'admin'],
|
||||
'crm-tp' => ['name' => 'ADMIN CRM > Reiseprogramme' , 'color' => 'admin'],
|
||||
'crm-tp-pr' => ['name' => 'ADMIN CRM > Reiseprogramme > Programme' , 'color' => 'admin'],
|
||||
'crm-tp-dr' => ['name' => 'ADMIN CRM > Reiseprogramme > Vorlagen' , 'color' => 'admin'],
|
||||
'crm-tp-tc' => ['name' => 'ADMIN CRM > Reiseprogramme > Inhalte' , 'color' => 'admin'],
|
||||
'crm-bo' => ['name' => 'ADMIN CRM > Buchungen' , 'color' => 'admin'],
|
||||
'crm-bo-re' => ['name' => 'ADMIN CRM > Buchungen > Übersicht' , 'color' => 'admin'],
|
||||
'crm-bo-bo' => ['name' => 'ADMIN CRM > Buchungen > Buchungen' , 'color' => 'admin'],
|
||||
'crm-bo-le' => ['name' => 'ADMIN CRM > Buchungen > Anfragen' , 'color' => 'admin'],
|
||||
'crm-bo-cu' => ['name' => 'ADMIN CRM > Buchungen > Kunden' , 'color' => 'admin'],
|
||||
'crm-cm' => ['name' => 'ADMIN CRM > Kundenverwaltung' , 'color' => 'admin'],
|
||||
'crm-cm-cf' => ['name' => 'ADMIN CRM > Kundenverwaltung > Kunden (FeWo)' , 'color' => 'admin'],
|
||||
'crm-cm-bf' => ['name' => 'ADMIN CRM > Kundenverwaltung > Buchungen (FeWo)' , 'color' => 'admin'],
|
||||
'crm-mail' => ['name' => 'ADMIN CRM > E-Mails' , 'color' => 'admin'],
|
||||
'crm-mail-le' => ['name' => 'ADMIN CRM > E-Mails > Anfragen' , 'color' => 'admin'],
|
||||
'crm-mail-bo' => ['name' => 'ADMIN CRM > E-Mails > Buchungen' , 'color' => 'admin'],
|
||||
'crm-mail-bf' => ['name' => 'ADMIN CRM > E-Mails > Buchungen (Fewo)' , 'color' => 'admin'],
|
||||
'crm-iq-tl' => ['name' => 'ADMIN CRM > Reisebausteine' , 'color' => 'admin'],
|
||||
'crm-iq-tl-pro' => ['name' => 'ADMIN CRM > Reisebausteine > Programm' , 'color' => 'admin'],
|
||||
'crm-iq-tl-gp' => ['name' => 'ADMIN CRM > Reisebausteine > Gruppe' , 'color' => 'admin'],
|
||||
'crm-iq-tl-it' => ['name' => 'ADMIN CRM > Reisebausteine > Baustein' , 'color' => 'admin'],
|
||||
'crm-old-cm' => ['name' => 'ADMIN CRM altes System > Kundenverwaltung' , 'color' => 'info'],
|
||||
'cms' => ['name' => 'ADMIN CMS' , 'color' => 'secondary'],
|
||||
'cms-iq-assets' => ['name' => 'ADMIN CMS > Medien' , 'color' => 'secondary'],
|
||||
'cms-tg' => ['name' => 'ADMIN CMS > Reiseführer' , 'color' => 'secondary'],
|
||||
'cms-fewo' => ['name' => 'ADMIN CMS > FeWo' , 'color' => 'secondary'],
|
||||
'cms-book' => ['name' => 'ADMIN CMS > Buchungen' , 'color' => 'secondary'],
|
||||
'cms-fb' => ['name' => 'ADMIN CMS > Feedback' , 'color' => 'secondary'],
|
||||
'cms-nw' => ['name' => 'ADMIN CMS > News' , 'color' => 'secondary'],
|
||||
'cms-aq' => ['name' => 'ADMIN CMS > Fragen & Antworten' , 'color' => 'secondary'],
|
||||
'cms-sb' => ['name' => 'ADMIN CMS > Sidebar' , 'color' => 'secondary'],
|
||||
'cms-cn' => ['name' => 'ADMIN CMS > Inhalte' , 'color' => 'secondary'],
|
||||
'cms-cn-in' => ['name' => 'ADMIN CMS > Inhalte > Infos' , 'color' => 'secondary'],
|
||||
'cms-cn-al' => ['name' => 'ADMIN CMS > Inhalte > Inhalte' , 'color' => 'secondary'],
|
||||
'cms-cn-au' => ['name' => 'ADMIN CMS > Inhalte > Autor' , 'color' => 'secondary'],
|
||||
'crm' => ['name' => 'ADMIN CRM ', 'color' => 'admin'],
|
||||
'crm-tp' => ['name' => 'ADMIN CRM > Reiseprogramme', 'color' => 'admin'],
|
||||
'crm-tp-pr' => ['name' => 'ADMIN CRM > Reiseprogramme > Programme', 'color' => 'admin'],
|
||||
'crm-tp-dr' => ['name' => 'ADMIN CRM > Reiseprogramme > Vorlagen', 'color' => 'admin'],
|
||||
'crm-tp-tc' => ['name' => 'ADMIN CRM > Reiseprogramme > Inhalte', 'color' => 'admin'],
|
||||
'crm-bo' => ['name' => 'ADMIN CRM > Buchungen', 'color' => 'admin'],
|
||||
'crm-bo-re' => ['name' => 'ADMIN CRM > Buchungen > Übersicht', 'color' => 'admin'],
|
||||
'crm-bo-bo' => ['name' => 'ADMIN CRM > Buchungen > Buchungen', 'color' => 'admin'],
|
||||
'crm-bo-le' => ['name' => 'ADMIN CRM > Buchungen > Anfragen', 'color' => 'admin'],
|
||||
'crm-bo-cu' => ['name' => 'ADMIN CRM > Buchungen > Kunden', 'color' => 'admin'],
|
||||
'crm-cm' => ['name' => 'ADMIN CRM > Kundenverwaltung', 'color' => 'admin'],
|
||||
'crm-cm-cf' => ['name' => 'ADMIN CRM > Kundenverwaltung > Kunden (FeWo)', 'color' => 'admin'],
|
||||
'crm-cm-bf' => ['name' => 'ADMIN CRM > Kundenverwaltung > Buchungen (FeWo)', 'color' => 'admin'],
|
||||
'crm-mail' => ['name' => 'ADMIN CRM > E-Mails', 'color' => 'admin'],
|
||||
'crm-mail-le' => ['name' => 'ADMIN CRM > E-Mails > Anfragen', 'color' => 'admin'],
|
||||
'crm-mail-bo' => ['name' => 'ADMIN CRM > E-Mails > Buchungen', 'color' => 'admin'],
|
||||
'crm-mail-bf' => ['name' => 'ADMIN CRM > E-Mails > Buchungen (Fewo)', 'color' => 'admin'],
|
||||
'crm-iq-tl' => ['name' => 'ADMIN CRM > Reisebausteine', 'color' => 'admin'],
|
||||
'crm-iq-tl-pro' => ['name' => 'ADMIN CRM > Reisebausteine > Programm', 'color' => 'admin'],
|
||||
'crm-iq-tl-gp' => ['name' => 'ADMIN CRM > Reisebausteine > Gruppe', 'color' => 'admin'],
|
||||
'crm-iq-tl-it' => ['name' => 'ADMIN CRM > Reisebausteine > Baustein', 'color' => 'admin'],
|
||||
'crm-old-cm' => ['name' => 'ADMIN CRM altes System > Kundenverwaltung', 'color' => 'info'],
|
||||
'cms' => ['name' => 'ADMIN CMS', 'color' => 'secondary'],
|
||||
'cms-iq-assets' => ['name' => 'ADMIN CMS > Medien', 'color' => 'secondary'],
|
||||
'cms-tg' => ['name' => 'ADMIN CMS > Reiseführer', 'color' => 'secondary'],
|
||||
'cms-fewo' => ['name' => 'ADMIN CMS > FeWo', 'color' => 'secondary'],
|
||||
'cms-book' => ['name' => 'ADMIN CMS > Buchungen', 'color' => 'secondary'],
|
||||
'cms-fb' => ['name' => 'ADMIN CMS > Feedback', 'color' => 'secondary'],
|
||||
'cms-nw' => ['name' => 'ADMIN CMS > News', 'color' => 'secondary'],
|
||||
'cms-aq' => ['name' => 'ADMIN CMS > Fragen & Antworten', 'color' => 'secondary'],
|
||||
'cms-sb' => ['name' => 'ADMIN CMS > Sidebar', 'color' => 'secondary'],
|
||||
'cms-cn' => ['name' => 'ADMIN CMS > Inhalte', 'color' => 'secondary'],
|
||||
'cms-cn-in' => ['name' => 'ADMIN CMS > Inhalte > Infos', 'color' => 'secondary'],
|
||||
'cms-cn-al' => ['name' => 'ADMIN CMS > Inhalte > Inhalte', 'color' => 'secondary'],
|
||||
'cms-cn-au' => ['name' => 'ADMIN CMS > Inhalte > Autor', 'color' => 'secondary'],
|
||||
'cms-cn-co' => ['name' => 'ADMIN CMS > Inhalte > Länder', 'color' => 'secondary'],
|
||||
'cms-newsletter' => ['name' => 'ADMIN CMS > Newsletter', 'color' => 'secondary'],
|
||||
'crm-nav-api' => ['name' => 'ADMIN CRM > Navigation API', 'color' => 'secondary'],
|
||||
],
|
||||
2 => [
|
||||
'sua-bo-n-edit' => ['name' => 'SUPERADMIN > Buchungen > Notizen > bearbeiten' , 'color' => 'secondary'],
|
||||
'sua-fewo-n-edit' => ['name' => 'SUPERADMIN > FeWo > Notizen > bearbeiten' , 'color' => 'secondary'],
|
||||
'sua-st' => ['name' => 'SUPERADMIN > Einstellungen' , 'color' => 'superadmin'],
|
||||
'sua-st-al' => ['name' => 'SUPERADMIN > Einstellungen > Airline' , 'color' => 'superadmin'],
|
||||
'sua-st-ap' => ['name' => 'SUPERADMIN > Einstellungen > Airport' , 'color' => 'superadmin'],
|
||||
'sua-st-em' => ['name' => 'SUPERADMIN > Einstellungen > E-Mails' , 'color' => 'superadmin'],
|
||||
'sua-st-ke' => ['name' => 'SUPERADMIN > Einstellungen > Keywords' , 'color' => 'superadmin'],
|
||||
'sua-st-sp' => ['name' => 'SUPERADMIN > Einstellungen > Leistungsträger' , 'color' => 'superadmin'],
|
||||
'sua-st-tn' => ['name' => 'SUPERADMIN > Einstellungen > Nationalitäten' , 'color' => 'superadmin'],
|
||||
'sua-st-co' => ['name' => 'SUPERADMIN > Einstellungen > Reiseländer' , 'color' => 'superadmin'],
|
||||
'sua-st-tp' => ['name' => 'SUPERADMIN > Einstellungen > Reiseprogramme' , 'color' => 'superadmin'],
|
||||
'sua-st-tpl' => ['name' => 'SUPERADMIN > Einstellungen > Reiseorte' , 'color' => 'superadmin'],
|
||||
'sua-st-bs' => ['name' => 'SUPERADMIN > Einstellungen > Reisestatus' , 'color' => 'superadmin'],
|
||||
'sua-st-tc' => ['name' => 'SUPERADMIN > Einstellungen > Veranstalter' , 'color' => 'superadmin'],
|
||||
'sua-st-tca' => ['name' => 'SUPERADMIN > Einstellungen > Reiseart' , 'color' => 'superadmin'],
|
||||
'sua-st-tap' => ['name' => 'SUPERADMIN > Einstellungen > Zielflughafen' , 'color' => 'superadmin'],
|
||||
'sua-st-in' => ['name' => 'SUPERADMIN > Einstellungen > Versicherungen' , 'color' => 'superadmin'],
|
||||
'sua-st-ca' => ['name' => 'SUPERADMIN > Einstellungen > Kategorien' , 'color' => 'superadmin'],
|
||||
'sua-st-tgn' => ['name' => 'SUPERADMIN > Einstellungen > Reisehinweise' , 'color' => 'superadmin'],
|
||||
'sua-re' => ['name' => 'SUPERADMIN > Export' , 'color' => 'superadmin'],
|
||||
'sua-re-bo' => ['name' => 'SUPERADMIN > Export > Buchungen' , 'color' => 'superadmin'],
|
||||
'sua-re-pp' => ['name' => 'SUPERADMIN > Export > Leistungsträger' , 'color' => 'superadmin'],
|
||||
'sua-re-fw' => ['name' => 'SUPERADMIN > Export > Fewo' , 'color' => 'superadmin'],
|
||||
'sua-re-le' => ['name' => 'SUPERADMIN > Export > Anfragen' , 'color' => 'superadmin'],
|
||||
'sua-ur-rt' => ['name' => 'SUPERADMIN > User Rechte' , 'color' => 'danger'],
|
||||
'cms-cn-co' => ['name' => 'ADMIN CMS > Inhalte > Länder' , 'color' => 'secondary'],
|
||||
'sua-bo-n-edit' => ['name' => 'SUPERADMIN > Buchungen > Notizen > bearbeiten', 'color' => 'secondary'],
|
||||
'sua-fewo-n-edit' => ['name' => 'SUPERADMIN > FeWo > Notizen > bearbeiten', 'color' => 'secondary'],
|
||||
'sua-st' => ['name' => 'SUPERADMIN > Einstellungen', 'color' => 'superadmin'],
|
||||
'sua-st-al' => ['name' => 'SUPERADMIN > Einstellungen > Airline', 'color' => 'superadmin'],
|
||||
'sua-st-ap' => ['name' => 'SUPERADMIN > Einstellungen > Airport', 'color' => 'superadmin'],
|
||||
'sua-st-em' => ['name' => 'SUPERADMIN > Einstellungen > E-Mails', 'color' => 'superadmin'],
|
||||
'sua-st-ke' => ['name' => 'SUPERADMIN > Einstellungen > Keywords', 'color' => 'superadmin'],
|
||||
'sua-st-sp' => ['name' => 'SUPERADMIN > Einstellungen > Leistungsträger', 'color' => 'superadmin'],
|
||||
'sua-st-tn' => ['name' => 'SUPERADMIN > Einstellungen > Nationalitäten', 'color' => 'superadmin'],
|
||||
'sua-st-co' => ['name' => 'SUPERADMIN > Einstellungen > Reiseländer', 'color' => 'superadmin'],
|
||||
'sua-st-tp' => ['name' => 'SUPERADMIN > Einstellungen > Reiseprogramme', 'color' => 'superadmin'],
|
||||
'sua-st-tpl' => ['name' => 'SUPERADMIN > Einstellungen > Reiseorte', 'color' => 'superadmin'],
|
||||
'sua-st-bs' => ['name' => 'SUPERADMIN > Einstellungen > Reisestatus', 'color' => 'superadmin'],
|
||||
'sua-st-tc' => ['name' => 'SUPERADMIN > Einstellungen > Veranstalter', 'color' => 'superadmin'],
|
||||
'sua-st-tca' => ['name' => 'SUPERADMIN > Einstellungen > Reiseart', 'color' => 'superadmin'],
|
||||
'sua-st-tap' => ['name' => 'SUPERADMIN > Einstellungen > Zielflughafen', 'color' => 'superadmin'],
|
||||
'sua-st-in' => ['name' => 'SUPERADMIN > Einstellungen > Versicherungen', 'color' => 'superadmin'],
|
||||
'sua-st-ca' => ['name' => 'SUPERADMIN > Einstellungen > Kategorien', 'color' => 'superadmin'],
|
||||
'sua-st-tgn' => ['name' => 'SUPERADMIN > Einstellungen > Reisehinweise', 'color' => 'superadmin'],
|
||||
'sua-re' => ['name' => 'SUPERADMIN > Export', 'color' => 'superadmin'],
|
||||
'sua-re-bo' => ['name' => 'SUPERADMIN > Export > Buchungen', 'color' => 'superadmin'],
|
||||
'sua-re-pp' => ['name' => 'SUPERADMIN > Export > Leistungsträger', 'color' => 'superadmin'],
|
||||
'sua-re-fw' => ['name' => 'SUPERADMIN > Export > Fewo', 'color' => 'superadmin'],
|
||||
'sua-re-le' => ['name' => 'SUPERADMIN > Export > Anfragen', 'color' => 'superadmin'],
|
||||
'sua-ur-rt' => ['name' => 'SUPERADMIN > User Rechte', 'color' => 'danger'],
|
||||
|
||||
],
|
||||
],
|
||||
'roles' => [
|
||||
|
|
@ -76,4 +79,4 @@ return [
|
|||
1 => 'Admin',
|
||||
2 => 'SuperAdmin'
|
||||
]
|
||||
];
|
||||
];
|
||||
|
|
|
|||
|
|
@ -0,0 +1,86 @@
|
|||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
class CreateNewsletterContactsTable extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function up()
|
||||
{
|
||||
Schema::create('newsletter_contacts', function (Blueprint $table) {
|
||||
$table->id();
|
||||
|
||||
// Kontaktdaten
|
||||
$table->string('email')->index();
|
||||
$table->string('firstname')->nullable();
|
||||
$table->string('lastname')->nullable();
|
||||
|
||||
// Newsletter-Gruppen (kann mehrere sein)
|
||||
$table->boolean('group_kulturreisen')->default(false)->index();
|
||||
$table->boolean('group_ferienwohnungen')->default(false)->index();
|
||||
|
||||
// Status & Herkunft
|
||||
$table->enum('source', [
|
||||
'booking_kulturreisen',
|
||||
'booking_ferienwohnungen',
|
||||
'newsletter_signup',
|
||||
'manual',
|
||||
'import'
|
||||
])->index();
|
||||
|
||||
$table->enum('status', [
|
||||
'active',
|
||||
'inactive',
|
||||
'unsubscribed',
|
||||
'bounced'
|
||||
])->default('active')->index();
|
||||
|
||||
// Tracking-Informationen
|
||||
$table->timestamp('subscribed_at')->nullable();
|
||||
$table->timestamp('unsubscribed_at')->nullable();
|
||||
$table->timestamp('last_booking_at')->nullable();
|
||||
|
||||
// Statistiken
|
||||
$table->integer('total_bookings_kulturreisen')->default(0);
|
||||
$table->integer('total_bookings_ferienwohnungen')->default(0);
|
||||
|
||||
// Referenzen zu Originaldaten
|
||||
$table->integer('customer_id')->nullable()->index()->comment('Referenz zu customer Tabelle für Kulturreisen');
|
||||
$table->integer('travel_user_id')->nullable()->index()->comment('Referenz zu travel_users Tabelle für Ferienwohnungen');
|
||||
|
||||
// Sync-Informationen
|
||||
$table->timestamp('last_synced_at')->nullable();
|
||||
$table->string('sync_hash')->nullable()->comment('Hash für Duplikat-Erkennung');
|
||||
|
||||
// Notizen & Zusatzinformationen
|
||||
$table->text('notes')->nullable();
|
||||
|
||||
$table->timestamps();
|
||||
$table->softDeletes();
|
||||
|
||||
// Unique Index auf E-Mail (nur für nicht gelöschte Einträge)
|
||||
$table->unique(['email', 'deleted_at'], 'newsletter_email_unique');
|
||||
|
||||
// Composite Index für häufige Abfragen mit kürzeren Namen
|
||||
$table->index(['status', 'group_kulturreisen', 'group_ferienwohnungen'], 'newsletter_status_groups_idx');
|
||||
$table->index(['source', 'status'], 'newsletter_source_status_idx');
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function down()
|
||||
{
|
||||
Schema::dropIfExists('newsletter_contacts');
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -0,0 +1,51 @@
|
|||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
class CreateNewsletterLogsTable extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function up()
|
||||
{
|
||||
Schema::create('newsletter_logs', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->foreignId('newsletter_contact_id')->constrained()->onDelete('cascade');
|
||||
|
||||
$table->enum('action', [
|
||||
'subscribed',
|
||||
'unsubscribed',
|
||||
'booking_added',
|
||||
'status_changed',
|
||||
'group_changed',
|
||||
'email_sent',
|
||||
'bounced'
|
||||
]);
|
||||
|
||||
$table->string('description')->nullable();
|
||||
$table->json('metadata')->nullable();
|
||||
|
||||
$table->integer('user_id')->nullable()->comment('Admin-User der die Aktion ausgeführt hat');
|
||||
|
||||
$table->timestamps();
|
||||
|
||||
$table->index(['newsletter_contact_id', 'created_at']);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function down()
|
||||
{
|
||||
Schema::dropIfExists('newsletter_logs');
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -0,0 +1,32 @@
|
|||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
class AddLastTravelEndDateToNewsletterContactsTable extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function up()
|
||||
{
|
||||
Schema::table('newsletter_contacts', function (Blueprint $table) {
|
||||
$table->dateTime('last_travel_end_date')->nullable()->after('last_booking_at');
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function down()
|
||||
{
|
||||
Schema::table('newsletter_contacts', function (Blueprint $table) {
|
||||
$table->dropColumn('last_travel_end_date');
|
||||
});
|
||||
}
|
||||
}
|
||||
312
dev/frontend-navigation/BACKEND-UI.md
Normal file
|
|
@ -0,0 +1,312 @@
|
|||
# Backend-UI für Navigation API
|
||||
|
||||
Die Backend-UI bietet eine benutzerfreundliche grafische Oberfläche zur Verwaltung und Visualisierung des Navigationsbaums **genau wie im Frontend (header.html.twig)**.
|
||||
|
||||
## Zugriff
|
||||
|
||||
**URL:** `/navigation-api`
|
||||
**Permission:** `crm-nav-api`
|
||||
**Menü:** Navigation API (Seitenmenü)
|
||||
|
||||
## Frontend-Struktur
|
||||
|
||||
Die Backend-UI zeigt den Navigationsbaum **exakt wie im Frontend** mit allen Bereichen:
|
||||
|
||||
### 🌍 Länder-Navigation
|
||||
|
||||
- ✅ **Länderseiten** (Pages mit `country_id`) als Root-Level
|
||||
- ✅ **Sortierung** nach `order` und `title`
|
||||
- ✅ **Gruppierung** der Children nach `beforeTitle` (Haupt / Infos)
|
||||
- ✅ **titleShort** wird verwendet statt vollständigem Titel
|
||||
- ✅ Toggle zum Auf-/Zuklappen der Children
|
||||
|
||||
### 🏠 USEDOM Ferienwohnungen
|
||||
|
||||
- ✅ **Ferienwohnungs-Übersicht** als Hauptseite
|
||||
- ✅ **Einzelne FeWos** als Children
|
||||
- ✅ Sortiert nach `order` und `title`
|
||||
|
||||
### 📑 Mehr-Menü Seiten
|
||||
|
||||
- ✅ **Über uns**
|
||||
- ✅ **Reiseversicherung**
|
||||
- ✅ **Reiseführer** (mit möglichen Children)
|
||||
- ✅ **Reisemagazin** (mit möglichen Children)
|
||||
- ✅ **Reisenews** (mit möglichen Children)
|
||||
|
||||
### Allgemein
|
||||
|
||||
- ✅ **Ausgeblendete Pages** werden angezeigt (Badge "Ausgeblendet")
|
||||
- ✅ **Section-Separators** trennen die Bereiche visuell
|
||||
|
||||
## Features
|
||||
|
||||
### 📊 Live-Statistiken
|
||||
|
||||
Oben auf der Seite werden wichtige Kennzahlen angezeigt:
|
||||
|
||||
- **Gesamt Seiten:** Alle Pages in der Datenbank
|
||||
- **Aktive Seiten:** Nur sichtbare und aktive Pages
|
||||
- **Reiseprogramme:** Anzahl der TravelProgram-Pages
|
||||
- **Länderseiten:** Anzahl der Country-Pages
|
||||
|
||||
Die Statistiken werden beim Laden der Seite automatisch aktualisiert.
|
||||
|
||||
### 🌳 Interaktiver Navigationsbaum
|
||||
|
||||
Der Hauptbereich zeigt den hierarchischen Navigationsbaum mit folgenden Features:
|
||||
|
||||
#### Visuelle Darstellung
|
||||
|
||||
- **Icons:**
|
||||
|
||||
- ⭐ Stern (gelb) = Länderseite (Root-Level)
|
||||
- ✈️ Flugzeug = Reiseprogramm
|
||||
- 🏠 Haus = Ferienwohnung
|
||||
- ℹ️ Info-Circle = Infos-Gruppe
|
||||
- 📄 Dokument = Normale Seite
|
||||
|
||||
- **Badges:**
|
||||
|
||||
- 🔴 **Inaktiv** = Status ist 0
|
||||
- ⚠️ **Ausgeblendet** = show_in_navi ist 0 (wird trotzdem angezeigt!)
|
||||
- 🔵 **Reiseprogramm** = Hat TravelProgram
|
||||
- 💙 **FeWo** = Hat FewoLodging
|
||||
- 💚 **Land** = Hat Country
|
||||
- ⚪ **Gruppe: Infos** = beforeTitle ist gesetzt
|
||||
|
||||
- **Hierarchie:**
|
||||
|
||||
- **Root-Level:** Länderseiten (fett, grauer Hintergrund, blaue Linie links)
|
||||
- **Children:** Eingerückt, normale Darstellung
|
||||
- **Separator "Infos":** Grauer Block trennt Haupt- von Info-Seiten
|
||||
- Toggle-Buttons nur bei Länderseiten
|
||||
|
||||
- **Besonderheiten:**
|
||||
- **titleShort** wird angezeigt (wie im Frontend)
|
||||
- **Gruppierung** nach beforeTitle (Haupt, dann Infos)
|
||||
- **Ausgeblendete** Pages haben gelbes Badge aber sind sichtbar
|
||||
|
||||
#### Interaktion
|
||||
|
||||
- **Auf-/Zuklappen:**
|
||||
|
||||
- Klick auf Pfeil: Einzelnen Knoten auf-/zuklappen
|
||||
- "Alle aufklappen": Zeigt kompletten Baum
|
||||
- "Alle zuklappen": Zeigt nur Root-Ebene
|
||||
|
||||
- **Hover-Effekt:**
|
||||
- Hintergrund wird grau beim Überfahren mit der Maus
|
||||
|
||||
### 🔍 Suche
|
||||
|
||||
Die Suchfunktion durchsucht alle Navigationspunkte nach:
|
||||
|
||||
- **Titel:** Page-Titel
|
||||
- **Slug:** URL-freundlicher Name
|
||||
- **URL:** Vollständiger Pfad
|
||||
|
||||
**Verwendung:**
|
||||
|
||||
1. Suchbegriff eingeben
|
||||
2. Enter drücken oder auf Suchbutton klicken
|
||||
3. Gefundene Knoten werden gelb markiert
|
||||
4. Parent-Knoten werden automatisch aufgeklappt
|
||||
|
||||
### 🎯 Filter
|
||||
|
||||
**"Mit ausgeblendeten" / "Nur sichtbare"**
|
||||
|
||||
- **Mit ausgeblendeten (Standard):** Zeigt alle Pages inkl. ausgeblendete (show_in_navi=0)
|
||||
- **Nur sichtbare:** Zeigt nur Pages mit show_in_navi=1 (wie im Frontend)
|
||||
|
||||
Der aktive Filter wird durch die Button-Farbe angezeigt:
|
||||
|
||||
- Blau = Mit ausgeblendeten
|
||||
- Primär = Nur sichtbare
|
||||
|
||||
Ausgeblendete Pages werden mit einem gelben Badge "Ausgeblendet" markiert.
|
||||
|
||||
### 📥 Export
|
||||
|
||||
**JSON-Export-Funktion:**
|
||||
|
||||
- Klick auf "Export JSON" lädt eine JSON-Datei herunter
|
||||
- Dateiname: `navigation-tree-YYYY-MM-DD-HHMMSS.json`
|
||||
- Inhalt: Kompletter Navigationsbaum entsprechend aktuellem Filter
|
||||
- Format: Pretty-printed JSON mit UTF-8 Encoding
|
||||
|
||||
**Verwendungszwecke:**
|
||||
|
||||
- Backup der Navigationsstruktur
|
||||
- Import in andere Systeme
|
||||
- Analyse und Dokumentation
|
||||
- Debugging
|
||||
|
||||
### 🔄 Cache-Verwaltung
|
||||
|
||||
**Cache leeren:**
|
||||
|
||||
- Klick auf "Cache leeren"
|
||||
- Bestätigung erforderlich
|
||||
- Löscht alle gecachten Navigationsdaten
|
||||
- Wird automatisch nach 60 Minuten erneuert
|
||||
|
||||
**Wann Cache leeren?**
|
||||
|
||||
- Nach Änderungen an Pages
|
||||
- Nach Import/Export von Daten
|
||||
- Bei veralteten Anzeigen
|
||||
- Nach Struktur-Änderungen
|
||||
|
||||
## Technische Details
|
||||
|
||||
### API-Calls
|
||||
|
||||
Die UI nutzt folgende Endpunkte:
|
||||
|
||||
```javascript
|
||||
// Statistiken laden
|
||||
GET /navigation-api/stats
|
||||
|
||||
// Navigationsbaum laden (Frontend-Struktur)
|
||||
GET /navigation-api/data?include_hidden=0|1
|
||||
|
||||
// Suche durchführen
|
||||
GET /navigation-api/search?query=...
|
||||
|
||||
// Export starten (Frontend-Struktur)
|
||||
GET /navigation-api/export?include_hidden=0|1
|
||||
|
||||
// Cache leeren
|
||||
POST /navigation-api/clear-cache
|
||||
```
|
||||
|
||||
**Unterschied zu API-Endpunkten:**
|
||||
|
||||
- `/api/navigation/*` = Kompletter Baum (API)
|
||||
- `/navigation-api/*` = Frontend-Struktur (nur Länderseiten)
|
||||
|
||||
### Performance
|
||||
|
||||
- **Caching:** 60 Minuten Server-seitig
|
||||
- **Lazy Loading:** Kinder werden nur bei Bedarf gerendert
|
||||
- **Optimierte Queries:** Eager Loading von Relationships
|
||||
- **Frontend-Rendering:** jQuery-basiert, schnell auch bei 1000+ Knoten
|
||||
|
||||
### Browser-Kompatibilität
|
||||
|
||||
- ✅ Chrome/Edge (aktuell)
|
||||
- ✅ Firefox (aktuell)
|
||||
- ✅ Safari (aktuell)
|
||||
- ⚠️ IE11 (eingeschränkt)
|
||||
|
||||
## Styling
|
||||
|
||||
### Farben
|
||||
|
||||
- **Primary (Blau):** Reiseprogramme, aktive Aktionen
|
||||
- **Success (Grün):** Aktive Pages, Länder
|
||||
- **Info (Cyan):** Ferienwohnungen
|
||||
- **Warning (Gelb):** "Nicht in Navi", Suchmarkierungen
|
||||
- **Danger (Rot):** Inaktive Pages, Löschen-Aktionen
|
||||
|
||||
### Responsive Design
|
||||
|
||||
Die UI ist responsive und funktioniert auf verschiedenen Bildschirmgrößen:
|
||||
|
||||
- **Desktop (>1200px):** Volle Features, 4 Statistik-Karten
|
||||
- **Tablet (768-1199px):** 2 Statistik-Karten pro Zeile
|
||||
- **Mobile (<768px):** 1 Statistik-Karte pro Zeile, vereinfachte Toolbar
|
||||
|
||||
## Shortcuts
|
||||
|
||||
Keine Keyboard-Shortcuts implementiert (noch).
|
||||
|
||||
## Bekannte Einschränkungen
|
||||
|
||||
1. **Sehr große Bäume (>10.000 Knoten):**
|
||||
|
||||
- Kann zu langsamem Rendering führen
|
||||
- Empfehlung: Filter verwenden
|
||||
|
||||
2. **Suche:**
|
||||
|
||||
- Nur client-seitig, keine Server-Suche
|
||||
- Bei vielen Ergebnissen kann es unübersichtlich werden
|
||||
|
||||
3. **Keine Bearbeitung:**
|
||||
- Nur Anzeige, keine Inline-Bearbeitung
|
||||
- Änderungen müssen über CMS erfolgen
|
||||
|
||||
## Zukünftige Erweiterungen
|
||||
|
||||
Mögliche Features für die Zukunft:
|
||||
|
||||
- [ ] Drag & Drop zum Verschieben von Knoten
|
||||
- [ ] Inline-Bearbeitung von Titeln
|
||||
- [ ] Bulk-Operationen (Status ändern, löschen, etc.)
|
||||
- [ ] Mehr Filter-Optionen (Template, Level, etc.)
|
||||
- [ ] Keyboard-Shortcuts
|
||||
- [ ] Pagination für sehr große Bäume
|
||||
- [ ] Graphische Visualisierung (D3.js Tree)
|
||||
- [ ] Breadcrumb-Anzeige für selektierten Knoten
|
||||
- [ ] Export in andere Formate (CSV, XML)
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Problem: Baum lädt nicht
|
||||
|
||||
**Lösung:**
|
||||
|
||||
1. Browser-Konsole öffnen (F12)
|
||||
2. Fehler prüfen
|
||||
3. Netzwerk-Tab prüfen: Status-Code der API-Calls
|
||||
4. Backend-Logs prüfen
|
||||
|
||||
### Problem: Statistiken zeigen "-"
|
||||
|
||||
**Lösung:**
|
||||
|
||||
1. API-Endpunkt `/navigation-api/stats` prüfen
|
||||
2. Browser-Konsole auf Fehler prüfen
|
||||
3. Permission `crm-tp-na` prüfen
|
||||
|
||||
### Problem: Cache wird nicht geleert
|
||||
|
||||
**Lösung:**
|
||||
|
||||
1. CSRF-Token prüfen
|
||||
2. POST-Request erfolgreich? (Netzwerk-Tab)
|
||||
3. Server-Logs prüfen
|
||||
4. Cache-System aktiv? (config/cache.php)
|
||||
|
||||
### Problem: Suche funktioniert nicht
|
||||
|
||||
**Lösung:**
|
||||
|
||||
1. JavaScript-Fehler in Konsole?
|
||||
2. jQuery geladen?
|
||||
3. Suchbegriff korrekt eingegeben?
|
||||
|
||||
## Support
|
||||
|
||||
Bei Problemen oder Fragen:
|
||||
|
||||
1. Dokumentation lesen: `dev/frontend-navigation/README.md`
|
||||
2. API-Dokumentation: `dev/frontend-navigation/navigation-api.md`
|
||||
3. Test-Tools verwenden: `dev/frontend-navigation/test-api.html`
|
||||
|
||||
## Changelog
|
||||
|
||||
### Version 1.0 (Initial Release)
|
||||
|
||||
- ✅ Navigationsbaum-Visualisierung
|
||||
- ✅ Live-Statistiken
|
||||
- ✅ Suche
|
||||
- ✅ Filter (Alle/Aktive)
|
||||
- ✅ Export (JSON)
|
||||
- ✅ Cache-Verwaltung
|
||||
- ✅ Responsive Design
|
||||
- ✅ Icon-basierte Typerkennung
|
||||
- ✅ Badge-System für Status
|
||||
256
dev/frontend-navigation/KernelControllerListener.php
Normal file
|
|
@ -0,0 +1,256 @@
|
|||
<?php
|
||||
|
||||
/**
|
||||
* @author Ulrich Hecht <ulrich.hecht@hecht-software.de>
|
||||
* @date 12/02/2016
|
||||
*/
|
||||
|
||||
namespace AppBundle\Listener;
|
||||
|
||||
|
||||
use AppBundle\AppBundle;
|
||||
use AppBundle\Controller\DefaultController;
|
||||
use AppBundle\Entity\Page;
|
||||
use AppBundle\Util;
|
||||
use Doctrine\ORM\EntityManager;
|
||||
use Doctrine\ORM\PersistentCollection;
|
||||
use Symfony\Bundle\FrameworkBundle\Routing\Router;
|
||||
use Symfony\Component\HttpFoundation\RedirectResponse;
|
||||
use Symfony\Component\HttpFoundation\Session\Session;
|
||||
use Symfony\Component\HttpKernel\Controller\ControllerResolverInterface;
|
||||
use Symfony\Component\HttpKernel\Event\FilterControllerEvent;
|
||||
use Symfony\Component\HttpKernel\Exception\HttpException;
|
||||
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
|
||||
|
||||
class KernelControllerListener
|
||||
{
|
||||
private $em;
|
||||
private $controllerResolver;
|
||||
|
||||
public function __construct(EntityManager $entityManager, ControllerResolverInterface $controllerResolver)
|
||||
{
|
||||
$this->em = $entityManager;
|
||||
$this->controllerResolver = $controllerResolver;
|
||||
}
|
||||
|
||||
private function setSessionAttributeByTime($request, $key)
|
||||
{
|
||||
|
||||
$session = $request->getSession();
|
||||
$session->set('_open_side_about', '');
|
||||
$session->set('_open_side_search', '');
|
||||
|
||||
if ($key === 'default') { //is default visit
|
||||
if (!$session->get('default_visit')) { //first visit
|
||||
$session->set('default_visit', true);
|
||||
$session->set('_open_side_about', 'open');
|
||||
$session->set('_open_side_search', 'open');
|
||||
}
|
||||
}
|
||||
|
||||
if ($key === 'api') { //is api = Reiseführer
|
||||
if (!$session->get('api_visit')) { //first visit
|
||||
$session->set('api_visit', true);
|
||||
$session->set('_open_side_about', 'open');
|
||||
}
|
||||
}
|
||||
}
|
||||
public function onKernelController(FilterControllerEvent $event)
|
||||
{
|
||||
$request = $event->getRequest();
|
||||
|
||||
$session = $request->getSession();
|
||||
Util::setMySession('search_request_b', $session->get('search_request_b'));
|
||||
Util::setMySession('search_request_e', $session->get('search_request_e'));
|
||||
Util::setMySession('search_request_c', $session->get('search_request_c'));
|
||||
|
||||
if ($request->get('_controller') === 'AppBundle\Controller\DefaultController::homeAction') {
|
||||
$this->setSessionAttributeByTime($request, "default");
|
||||
}
|
||||
if ($request->get('_controller') == 'AppBundle\Controller\DefaultController::defaultAction') {
|
||||
|
||||
$repo = $this->em->getRepository('AppBundle:Page');
|
||||
$path = preg_replace('/^\/?(.*?)\/?$/', '$1', $request->getPathInfo());
|
||||
/** @var Page $node */
|
||||
$node = null;
|
||||
|
||||
// Try to find by url path. It's possible that the path consists of two parts:
|
||||
// - the beginning part represents a node
|
||||
// - the ending part represents a handler
|
||||
// e.g. /path/to/travel/program/buchen ("buchen" is the handler part)
|
||||
$pathArray = explode('/', $path);
|
||||
$restOfPath = '';
|
||||
$curPath = $path;
|
||||
//search for entry in new tree objects
|
||||
$api = Util::loadFromApi('cms/search', ['url' => $curPath]);
|
||||
while (!empty($pathArray)) {
|
||||
if (!$api) {
|
||||
$node = $repo->findOneBy(['realUrlPath' => '/' . $curPath]);
|
||||
if ($node) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
$restOfPath = '/' . array_pop($pathArray) . $restOfPath;
|
||||
$curPath = implode('/', $pathArray);
|
||||
}
|
||||
//find and try 301
|
||||
//find => to
|
||||
$redirects = [
|
||||
'/reisefuehrer/tuerkei' => 'tuerkei-reisen/reisefuehrer',
|
||||
'/reisefuehrer/jordanien' => 'jordanien-reisen/reisefuehrer',
|
||||
'/reisefuehrer/oman' => 'oman-reisen/reisefuehrer',
|
||||
'/reisefuehrer/israel' => 'israel-reisen/israel-reisefuehrer',
|
||||
'/reisefuehrer/aegypten' => 'aegypten-reisen/aegypten-reisefuehrer',
|
||||
'/reisemagazin/tuerkei-reisemagazin' => 'tuerkei-reisen/tuerkei-reisemagazin',
|
||||
'/reisemagazin/aegypten' => 'aegypten-reisen/aegypten-reisemagazin',
|
||||
'/reisemagazin/israel' => 'israel-reisen/israel-reisemagazin',
|
||||
'/reisemagazin/jordanien' => 'jordanien-reisen/jordanien-reisemagazin',
|
||||
'/reisemagazin/marokko' => 'marokko-urlaub/marokko-reisemagazin'
|
||||
|
||||
];
|
||||
//301
|
||||
foreach ($redirects as $find => $to) {
|
||||
if (strpos($restOfPath, $find) !== false) {
|
||||
$restOfPath = str_replace($find, $to, $restOfPath);
|
||||
$protocol = 'https';
|
||||
if (empty($_SERVER['HTTPS']) || $_SERVER['HTTPS'] === "off") {
|
||||
$protocol = 'http';
|
||||
}
|
||||
header("Location: " . $protocol . "://" . $_SERVER["HTTP_HOST"] . "/" . $restOfPath, true, 301);
|
||||
exit();
|
||||
}
|
||||
}
|
||||
//load content from API, is found by cms/search
|
||||
|
||||
if ($api) {
|
||||
$this->setSessionAttributeByTime($request, "api");
|
||||
$request->attributes->set('_controller', 'AppBundle:Cms:iqTravelGuide');
|
||||
$request->attributes->set('api', $api);
|
||||
$request->attributes->set('template', 'TravelGuide');
|
||||
try {
|
||||
$controller = $this->controllerResolver->getController($request);
|
||||
} catch (\LogicException $e) {
|
||||
// If there is no controller action, call the default action and pass the template name
|
||||
$request->attributes->set('_controller', 'AppBundle:Cms:Default');
|
||||
$request->attributes->set('template', "Default");
|
||||
}
|
||||
$event->setController($controller ?? $this->controllerResolver->getController($request));
|
||||
return;
|
||||
}
|
||||
$this->setSessionAttributeByTime($request, "default");
|
||||
|
||||
if (!$node) {
|
||||
// Now try to find a page by tracing a page node path using the page nodes' slugs
|
||||
|
||||
$pathArray = explode('/', $path);
|
||||
|
||||
while (!empty($pathArray)) {
|
||||
$restOfPath = '/' . implode('/', $pathArray);
|
||||
$slug = array_shift($pathArray);
|
||||
|
||||
$qb = $repo->createQueryBuilder('p');
|
||||
|
||||
$qb->where($qb->expr()->eq('p.slug', ':slug'));
|
||||
$qb->setParameter('slug', $slug);
|
||||
|
||||
if ($node != null) {
|
||||
$qb->andWhere($qb->expr()->eq('p.parent', ':parentId'));
|
||||
$qb->setParameter('parentId', $node->getId());
|
||||
} else {
|
||||
$qb->andWhere($qb->expr()->isNull('p.parent'));
|
||||
}
|
||||
|
||||
$qb->setMaxResults(1);
|
||||
$childNode = $qb->getQuery()->getOneOrNullResult();
|
||||
if (!$childNode) {
|
||||
$whitelist = [
|
||||
'buchen',
|
||||
'berechne-gesamtpreis',
|
||||
'show_nationality_country_text',
|
||||
'pdf',
|
||||
];
|
||||
if (!in_array($slug, $whitelist)) {
|
||||
throw new HttpException(404, 'Seite nicht gefunden: ' . $slug);
|
||||
}
|
||||
break;
|
||||
}
|
||||
if ($node) {
|
||||
|
||||
// Avoid database calls to parent later
|
||||
$childNode->setParent($node);
|
||||
}
|
||||
$node = $childNode;
|
||||
}
|
||||
if ($node && $node->getRealUrlPath() && $node->getRealUrlPath() != '/' . $path) {
|
||||
// If there realUrlPath is set and the slug path differs from realUrlPath, then the slug path is
|
||||
// not a valid URL. Otherwise, there would be two different URLs representing the same page.
|
||||
$event->setController(function () {
|
||||
throw new NotFoundHttpException('Invalid URL');
|
||||
});
|
||||
return;
|
||||
}
|
||||
}
|
||||
if (!$node) {
|
||||
// Search for a redirect entry
|
||||
$redirect = $this->em->getRepository('AppBundle:Redirect')->findOneBy(['sourceUrlPath' => '/' . $path]);
|
||||
if ($redirect) {
|
||||
$redirectUrl = $redirect->getPage()->getUrlPath();
|
||||
$event->setController(function () use ($redirectUrl) {
|
||||
return new RedirectResponse($redirectUrl, 301);
|
||||
});
|
||||
return;
|
||||
}
|
||||
}
|
||||
if ($node) {
|
||||
|
||||
if ($node->getStatus() == 0) {
|
||||
throw new NotFoundHttpException('Inactive page');
|
||||
}
|
||||
|
||||
$request->attributes->set('page', $node);
|
||||
|
||||
if ($node->getTravelProgram() != null && (
|
||||
$restOfPath == '/buchen' || $restOfPath == '/berechne-gesamtpreis' || $restOfPath == '/show_nationality_country_text')) {
|
||||
// Special case: Booking actions
|
||||
$request->attributes->set('travelProgramPage', $node);
|
||||
$request->attributes->set('action', $restOfPath);
|
||||
$request->attributes->set('_controller', 'AppBundle:Booking:index');
|
||||
} elseif ($restOfPath && $node->getTravelProgram() !== null && (
|
||||
$restOfPath === '/pdf')) {
|
||||
$request->attributes->set('_controller', 'AppBundle:Cms:pdf');
|
||||
} elseif ($node->getTravelProgram() !== null) {
|
||||
if ($node->getTravelProgram()->getStatus() == 0) {
|
||||
throw new \NotFoundHttpException('Inactive travel program');
|
||||
}
|
||||
$request->attributes->set('_controller', 'AppBundle:Cms:travelProgram');
|
||||
} elseif (
|
||||
$node->getFewoLodging() != null &&
|
||||
($restOfPath === '/buchen' || $restOfPath === '/berechne-gesamtpreis')
|
||||
) {
|
||||
$request->attributes->set('fewoTravelProgramPage', $node);
|
||||
$request->attributes->set('action', $restOfPath);
|
||||
$request->attributes->set('_controller', 'AppBundle:FewoBooking:index');
|
||||
} elseif ($node->getFewoLodging() !== null) {
|
||||
$request->attributes->set('fewoLodgingPage', $node);
|
||||
$request->attributes->set('action', $restOfPath);
|
||||
$request->attributes->set('_controller', 'AppBundle:Cms:fewoLodging');
|
||||
} else {
|
||||
$handler = $node->getTemplate() ? ucfirst($node->getTemplate()) : 'Default';
|
||||
$request->attributes->set('_controller', 'AppBundle:Cms:' . $handler);
|
||||
if ($node->getTemplate()) {
|
||||
try {
|
||||
$controller = $this->controllerResolver->getController($request);
|
||||
} catch (\LogicException $e) {
|
||||
// If there is no controller action, call the default action and pass the template name
|
||||
$request->attributes->set('_controller', 'AppBundle:Cms:Default');
|
||||
$request->attributes->set('template', $node->getTemplate());
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
$event->setController($controller ?? $this->controllerResolver->getController($request));
|
||||
}
|
||||
}
|
||||
}
|
||||
180
dev/frontend-navigation/PageRepository.php
Normal file
|
|
@ -0,0 +1,180 @@
|
|||
<?php
|
||||
|
||||
namespace AppBundle\Entity;
|
||||
|
||||
use Gedmo\Tree\Entity\Repository\NestedTreeRepository;
|
||||
use Doctrine\ORM\Query\Expr;
|
||||
|
||||
/**
|
||||
* PageRepository
|
||||
*
|
||||
* This class was generated by the Doctrine ORM. Add your own custom
|
||||
* repository methods below.
|
||||
*/
|
||||
class PageRepository extends NestedTreeRepository
|
||||
{
|
||||
/**
|
||||
* @param Page $page
|
||||
* @return Page[]|array
|
||||
*
|
||||
* @todo Optimize performance by adapting search algorithm's optimizations
|
||||
*/
|
||||
public function getChildrenWithTravelProgramsAndDates(Page $page)
|
||||
{
|
||||
$pages = $this->getChildrenQueryBuilder($page)
|
||||
->leftJoin('node.travelProgram', 'tp')
|
||||
->addSelect('tp')
|
||||
->andWhere('tp.status > 0')
|
||||
->andWhere('node.status > 0')
|
||||
->orderBy('node.order')
|
||||
->addOrderBy('tp.position')
|
||||
->addOrderBy('node.title')
|
||||
->getQuery()
|
||||
->execute();
|
||||
/** @var Page $childPage */
|
||||
foreach ($pages as &$childPage) {
|
||||
if ($childPage->getTravelProgram()) {
|
||||
$this->getEntityManager()->getRepository('AppBundle:TravelPeriod')->getTrueTravelPeriods(
|
||||
$childPage->getTravelProgram()
|
||||
);
|
||||
}
|
||||
}
|
||||
return $pages;
|
||||
}
|
||||
|
||||
public function findWithTravelProgramsOfCountry(TravelCountry $country)
|
||||
{
|
||||
return $this->createQueryBuilder('node')
|
||||
->innerJoin('node.travelProgram', 'tp')
|
||||
->innerJoin('tp.countries', 'c')
|
||||
->where('c.id = ' . $country->getId())
|
||||
->andWhere('node.status = 1')
|
||||
->andWhere('tp.status = 1')
|
||||
->getQuery()
|
||||
->execute()
|
||||
;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Page[]
|
||||
*/
|
||||
public function findOffers()
|
||||
{
|
||||
$ret = [];
|
||||
$countries = $this->getEntityManager()->getRepository('AppBundle:TravelCountry')->findAll();
|
||||
foreach ($countries as $country) {
|
||||
$ret = array_merge(
|
||||
$ret,
|
||||
$this->createQueryBuilder('node')
|
||||
->innerJoin('node.travelProgram', 'tp')
|
||||
->addSelect('tp')
|
||||
->innerJoin('tp.countries', 'c')
|
||||
->where('c.id = ' . $country->getId())
|
||||
->andWhere('node.status = 1')
|
||||
->andWhere('tp.status = 1')
|
||||
->orderBy('node.order')
|
||||
->addOrderBy('tp.position')
|
||||
->addOrderBy('node.title')
|
||||
->setMaxResults(3)
|
||||
->getQuery()
|
||||
->execute()
|
||||
);
|
||||
}
|
||||
shuffle($ret);
|
||||
return $ret;
|
||||
}
|
||||
|
||||
public function findCountryPages()
|
||||
{
|
||||
return $this->createQueryBuilder('node')
|
||||
->innerJoin('node.country', 'country')
|
||||
->where('node.status > 0')
|
||||
->andWhere('node.template = \'overview\'')
|
||||
->andWhere('node.lvl = 0')
|
||||
->andWhere('node.order > 0')
|
||||
->orderBy('node.order,node.title')
|
||||
->getQuery()
|
||||
->execute()
|
||||
;
|
||||
}
|
||||
|
||||
public function findTopCountryNavPages()
|
||||
{
|
||||
return $this->createQueryBuilder('node')
|
||||
->innerJoin('node.country', 'country')
|
||||
->leftJoin('node.children', 'childPage', Expr\Join::WITH, 'childPage.status > 0')
|
||||
->addSelect('childPage')
|
||||
->where('node.status > 0')
|
||||
->andWhere('node.template = \'overview\'')
|
||||
->andWhere('node.lvl = 0')
|
||||
->andWhere('node.order > 0')
|
||||
->orderBy('node.order,node.title, childPage.order, childPage.title')
|
||||
->getQuery()
|
||||
->execute()
|
||||
;
|
||||
}
|
||||
|
||||
public function findFeedbacks($rootPageId)
|
||||
{
|
||||
$qb = $this->createQueryBuilder('node');
|
||||
return $qb
|
||||
->where($qb->expr()->eq('node.parent', $rootPageId))
|
||||
->andWhere('node.showInNavi = 1')
|
||||
->andWhere('node.status = 1')
|
||||
->orderBy('node.order')
|
||||
->getQuery()
|
||||
->execute()
|
||||
;
|
||||
}
|
||||
|
||||
public function findParentsWithShowNav($rootPageId)
|
||||
{
|
||||
|
||||
$qb = $this->createQueryBuilder('node');
|
||||
$pages = $qb->innerJoin('node.travelProgram', 'tp')
|
||||
->addSelect('tp')
|
||||
->where($qb->expr()->eq('node.parent', $rootPageId))
|
||||
->andWhere('node.showInNavi = 1')
|
||||
->andWhere('node.status = 1')
|
||||
->andWhere('tp.status > 0')
|
||||
->orderBy('node.order')
|
||||
->getQuery()
|
||||
->execute();
|
||||
|
||||
foreach ($pages as &$childPage) {
|
||||
if ($childPage->getTravelProgram()) {
|
||||
// var_dump($childPage->getTravelProgram()->getId());
|
||||
// $this->getEntityManager()->getRepository('AppBundle:TravelPeriod')->getTrueTravelPeriods($childPage->getTravelProgram());
|
||||
}
|
||||
}
|
||||
return $pages;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Page $page
|
||||
*
|
||||
* @return Page[]|\Doctrine\Common\Collections\Collection
|
||||
*/
|
||||
public function getSiblings(Page $page)
|
||||
{
|
||||
$parent = $page->getParent();
|
||||
if (!$parent) {
|
||||
// On purpose, we don't treat root pages as if they were siblings
|
||||
return [];
|
||||
}
|
||||
$siblings = $parent->getChildren();
|
||||
foreach ($siblings as &$sibling) {
|
||||
$sibling->setParent($parent);
|
||||
}
|
||||
|
||||
// Da diese Methode nur für die Navigation verwendet wird, kann man hier vorfiltern
|
||||
$filteredSiblings = [];
|
||||
foreach ($siblings as &$sibling) {
|
||||
if ($sibling->getStatus() == 1 && $sibling->getShowInNavi() == 1) {
|
||||
$filteredSiblings[] = $sibling;
|
||||
}
|
||||
}
|
||||
|
||||
return $filteredSiblings;
|
||||
}
|
||||
}
|
||||
197
dev/frontend-navigation/README.md
Normal file
|
|
@ -0,0 +1,197 @@
|
|||
# Frontend Navigation - Backend API
|
||||
|
||||
Dieses Verzeichnis enthält die Dokumentation und Referenzimplementierungen für die Frontend-Navigation.
|
||||
|
||||
## Übersicht
|
||||
|
||||
Die Navigation-API stellt die komplette Seitenstruktur des CMS als hierarchischen Baum oder flache Liste zur Verfügung.
|
||||
|
||||
## Schnellstart
|
||||
|
||||
### Backend-UI (Admin-Interface)
|
||||
|
||||
Zugriff über: **Navigation API** im Seitenmenü (Permission: `crm-nav-api`)
|
||||
|
||||
Die Backend-UI zeigt die **komplette Frontend-Navigation**:
|
||||
|
||||
- 📊 Live-Statistiken (Gesamt, Aktiv, Programme, Länder)
|
||||
- 🌍 **Länder-Navigation** mit Children (gruppiert nach Haupt/Infos)
|
||||
- 🏠 **USEDOM Ferienwohnungen** mit allen FeWos
|
||||
- 📑 **Mehr-Menü Seiten** (Über uns, Reiseversicherung, Reiseführer, Reisemagazin, Reisenews)
|
||||
- 🔍 Volltext-Suche über Titel, Slug und URL
|
||||
- 🌳 Interaktiver Nested Tree mit Auf-/Zuklappen
|
||||
- 🎯 Filter (Mit ausgeblendeten/Nur sichtbare)
|
||||
- 📥 JSON-Export
|
||||
- 🔄 Cache-Verwaltung
|
||||
|
||||
### API-Endpunkte
|
||||
|
||||
```
|
||||
GET /api/navigation/tree - Kompletter Navigationsbaum
|
||||
GET /api/navigation/tree/active - Nur aktive Navigationspunkte
|
||||
GET /api/navigation/tree/{rootId} - Teilbaum ab einer bestimmten Page
|
||||
GET /api/navigation/flat - Flache Liste aller Pages
|
||||
GET /api/navigation/breadcrumb/{pageId} - Breadcrumb-Pfad
|
||||
```
|
||||
|
||||
### Beispiel-Verwendung
|
||||
|
||||
```javascript
|
||||
// Kompletten Navigationsbaum abrufen
|
||||
fetch("/api/navigation/tree")
|
||||
.then((response) => response.json())
|
||||
.then((data) => {
|
||||
console.log(data.data); // Navigationsbaum
|
||||
});
|
||||
```
|
||||
|
||||
## Dateien
|
||||
|
||||
- **README.md** - Diese Datei (Übersicht)
|
||||
- **BACKEND-UI.md** - Dokumentation der Backend-UI (Admin-Interface)
|
||||
- **navigation.md** - Dokumentation der Frontend-Routing-Logik (Symfony-basiert)
|
||||
- **navigation-api.md** - Vollständige API-Dokumentation mit Beispielen
|
||||
- **test-api.php** - CLI-Test-Script für alle API-Endpunkte
|
||||
- **test-api.html** - Browser-basierte interaktive Test-Oberfläche
|
||||
- **KernelControllerListener.php** - Referenz: Symfony KernelControllerListener
|
||||
- **PageRepository.php** - Referenz: Doctrine Page Repository
|
||||
|
||||
## Implementierung
|
||||
|
||||
Die API wurde im Laravel-Backend implementiert:
|
||||
|
||||
### Backend-Komponenten
|
||||
|
||||
**API-Layer:**
|
||||
|
||||
1. **API Controller:** `app/Http/Controllers/API/NavigationController.php`
|
||||
|
||||
- REST API-Endpunkte
|
||||
- JSON-Responses
|
||||
|
||||
2. **Web Controller:** `app/Http/Controllers/NavigationTreeController.php`
|
||||
|
||||
- Backend-UI (Admin-Interface)
|
||||
- Daten-Export und Cache-Management
|
||||
|
||||
3. **Service:** `app/Services/NavigationTreeService.php`
|
||||
|
||||
- Business-Logik
|
||||
- Rekursiver Baum-Aufbau
|
||||
- Caching (60 Min.)
|
||||
- Helper-Methoden (Breadcrumb, Node-Suche, etc.)
|
||||
|
||||
4. **Model:** `app/Models/Page.php`
|
||||
|
||||
- Eloquent Model für die Page-Entität
|
||||
- Relationships (parent, children, travel_program, etc.)
|
||||
|
||||
5. **Views:** `resources/views/navigation/index.blade.php`
|
||||
|
||||
- Interaktive Baum-Visualisierung
|
||||
- Suche und Filter
|
||||
- Statistiken
|
||||
|
||||
6. **Routes:**
|
||||
- API: `routes/api.php`
|
||||
- Web: `routes/web.php` (Permission: `crm-tp-na`)
|
||||
|
||||
## Features
|
||||
|
||||
### Navigationsbaum
|
||||
|
||||
- ✅ Hierarchische Struktur mit unbegrenzter Tiefe
|
||||
- ✅ Alle Page-Eigenschaften enthalten (SEO, Template, Status, etc.)
|
||||
- ✅ Beziehungen zu TravelProgram, FewoLodging, Country
|
||||
- ✅ Filterung nach Status und Sichtbarkeit
|
||||
- ✅ Sortierung nach Order und Title
|
||||
|
||||
### Zusätzliche Funktionen
|
||||
|
||||
- ✅ Breadcrumb-Generierung
|
||||
- ✅ Teilbaum-Abfrage
|
||||
- ✅ Flache Listen-Ansicht
|
||||
- ✅ Node-Zählung
|
||||
- ✅ URL-Pfad-Generierung
|
||||
|
||||
### Performance
|
||||
|
||||
- Effiziente Queries mit Eager Loading
|
||||
- Rekursive Baum-Konstruktion
|
||||
- Unterstützung für Nested Set Tree (lft/rgt)
|
||||
- Caching-ready
|
||||
|
||||
## Datenstruktur
|
||||
|
||||
Jeder Navigationspunkt enthält:
|
||||
|
||||
```json
|
||||
{
|
||||
"id": 1,
|
||||
"title": "Seitentitel",
|
||||
"slug": "seitentitel",
|
||||
"url": "/seitentitel",
|
||||
"status": 1,
|
||||
"show_in_navi": 1,
|
||||
"order": 1,
|
||||
"template": "default",
|
||||
"lvl": 0,
|
||||
"parent_id": null,
|
||||
"is_travel_program": false,
|
||||
"is_fewo_lodging": false,
|
||||
"is_country_page": false,
|
||||
"children": [],
|
||||
"has_children": false
|
||||
}
|
||||
```
|
||||
|
||||
Siehe `navigation-api.md` für eine vollständige Feldbeschreibung.
|
||||
|
||||
## Routing-Logik (Frontend)
|
||||
|
||||
Das Frontend verwendet einen `KernelControllerListener` (Symfony), der:
|
||||
|
||||
1. **API-Lookup** prüft (externe Reiseführer)
|
||||
2. **Datenbank-Suche** durchführt (Page-Entität)
|
||||
3. **Redirects** verarbeitet (301-Weiterleitungen)
|
||||
4. **Controller zuweist** basierend auf Content-Type
|
||||
|
||||
Siehe `navigation.md` für Details zur Routing-Logik.
|
||||
|
||||
## Unterschiede Frontend/Backend
|
||||
|
||||
| Aspekt | Frontend (Symfony) | Backend (Laravel) |
|
||||
| ---------- | ----------------------------- | -------------------------- |
|
||||
| Framework | Symfony 2.x | Laravel 5.x |
|
||||
| ORM | Doctrine | Eloquent |
|
||||
| Routing | KernelControllerListener | API Routes |
|
||||
| Tree-Logik | Nested Set + Parent Traversal | Parent-Child Relationships |
|
||||
| API | External (IQ API) | Internal (Laravel) |
|
||||
|
||||
## Wartung
|
||||
|
||||
### Neue Felder hinzufügen
|
||||
|
||||
1. Füge das Feld in `NavigationTreeService::buildNodeData()` hinzu
|
||||
2. Aktualisiere die Dokumentation in `navigation-api.md`
|
||||
|
||||
### Performance optimieren
|
||||
|
||||
1. Füge Caching in `NavigationTreeService` hinzu
|
||||
2. Verwende Eager Loading für Relations
|
||||
3. Implementiere Pagination für große Bäume
|
||||
|
||||
### Tests
|
||||
|
||||
Empfohlene Tests:
|
||||
|
||||
- Unit Tests für `NavigationTreeService`
|
||||
- API Tests für alle Endpunkte
|
||||
- Performance Tests für große Navigationsbäume
|
||||
|
||||
## Support
|
||||
|
||||
Bei Fragen zur API siehe:
|
||||
|
||||
- `navigation-api.md` - Vollständige API-Dokumentation
|
||||
- `navigation.md` - Frontend Routing-Dokumentation
|
||||
325
dev/frontend-navigation/header.html.twig
Normal file
|
|
@ -0,0 +1,325 @@
|
|||
<div id="topBar" class="">
|
||||
<div class="container-fluid">
|
||||
{% if content.info.office_important_note_active == 1 %}
|
||||
<ul class="top-links block">
|
||||
<li class="icon">
|
||||
<a class="dropdown-toggle no-text-underline" data-toggle="dropdown" data-hover="dropdown" href="#"><i class="fa fa-info"></i></a>
|
||||
<div class="dropdown-menu dropdown-menu-left dropdown-menu-infos">
|
||||
<div class="dropdown-menu-header">
|
||||
<span><i class="fa fa-info"></i> aktuelle Infos</span>
|
||||
</div>
|
||||
<div class="dropdown-menu-body">
|
||||
{{ content.info.office_important_note }}
|
||||
</div>
|
||||
</div>
|
||||
</li>
|
||||
<li style="overflow: hidden;width: 100%;">
|
||||
<div id="marquee" class="marquee"><span> {{ content.info.office_important_note }}</span></div>
|
||||
</li>
|
||||
</ul>
|
||||
{% endif %}
|
||||
<ul class="top-links block wrap" id="topNavAccordion">
|
||||
<li>
|
||||
<a class="dropdown-toggle no-text-underline collapsed" data-toggle="collapse" href="#collapseTopTravelDates" role="button" aria-expanded="false" aria-controls="collapseTopTravelDates">
|
||||
<i class="fa fa-plane"></i> Reisetermine
|
||||
<i class="fa fa-caret-collapse"></i>
|
||||
</a>
|
||||
<!-- ab Montag um 09:00 Uhr -->
|
||||
<!-- bis xxx Uhr -->
|
||||
<div class="dropdown-menu-infos collapse" id="collapseTopTravelDates">
|
||||
<div class="dropdown-menu-body">
|
||||
<div class="badge badge-default btn-block">
|
||||
<span class="text-default">{{ header_travel_program.title }} <br>
|
||||
</div>
|
||||
<table class="table table-condensed table-vertical-middle">
|
||||
<tr>
|
||||
<th class="text-left">Hinflug</th>
|
||||
<th class="text-left">Rückflug</th>
|
||||
<th class="text-left">Preis p. P.</th>
|
||||
</tr>
|
||||
|
||||
{% set last_name = "" %}
|
||||
{% for travel_date in header_travel_program.travelDates('header') if travel_date.status >= 0 %}
|
||||
{% if loop.index <= 6 %}
|
||||
{% if last_name != travel_date.name %}
|
||||
{% set last_name = travel_date.name %}
|
||||
<tr>
|
||||
<td class="text-left">{{ travel_date.start|date }}</td>
|
||||
<td class="text-left">{{ travel_date.end|date }}</td>
|
||||
<td class="text-left">
|
||||
<strong>
|
||||
{% if travel_date.prices[3] is defined %}
|
||||
{% if travel_date.prices[3].available == "1" %}
|
||||
{% if travel_date.prices[3].effectiveDiscountPrice %}
|
||||
<a href="{{ header_travel_program.page.urlPath }}" style="color: #558c55; text-decoration: underline;">
|
||||
ab {{ travel_date.prices[3].effectiveDiscountPrice|number_format }} €
|
||||
</a>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
</strong>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
</table>
|
||||
<a href="{{ header_travel_program.page.getUrlPathBefore }}">weitere Rundreisen ansehen</a>
|
||||
</div>
|
||||
</div>
|
||||
</li>
|
||||
<li>
|
||||
<a class="dropdown-toggle no-text-underline collapsed" data-toggle="collapse" href="#collapseTopPhone" role="button" aria-expanded="false" aria-controls="collapseTopPhone">
|
||||
<i class="fa fa-phone-square"></i> 030 - 700 94 100 •
|
||||
{% if(content.available.phone.active) %}
|
||||
<span class="text-success">erreichbar</span>
|
||||
{% else %}
|
||||
erreichbar
|
||||
{% endif %}
|
||||
{{ content.available.phone.content }}
|
||||
<i class="fa fa-caret-collapse"></i>
|
||||
</a>
|
||||
<!-- ab Montag um 09:00 Uhr -->
|
||||
<!-- bis xxx Uhr -->
|
||||
<div class="dropdown-menu-infos collapse" id="collapseTopPhone">
|
||||
<div class="dropdown-menu-body">
|
||||
<p><a href="tel:030 - 700 94 100" class="btn btn-secondary btn-sm btn-block text-center"><i class="fa fa-phone-square text-success" ></i> 030 - 700 94 100</a></p>
|
||||
<hr>
|
||||
<div class="badge badge-default btn-block">
|
||||
{% if(content.available.phone.active) %}
|
||||
<span class="text-success"><i class="fa fa-check-circle fa-lg"></i></span> Wir sind zur Zeit telefonisch zu erreichen.
|
||||
{% else %}
|
||||
<span class="text-danger"><i class="fa fa-times-circle fa-lg"></i></span> Wir sind zur Zeit telefonisch nicht zu erreichen.
|
||||
{% endif %}
|
||||
</div>
|
||||
<table class="table table-condensed table-vertical-middle">
|
||||
{% for key, val in phone %}
|
||||
<tr>
|
||||
<td class="text-left" style="width: 50%">{{ val.day }} <span class="text-muted pull-right"> {{ val.date }}</span></td>
|
||||
{% if val.active == 0 %}
|
||||
<td colspan="3">geschlossen</td>
|
||||
{% else %}
|
||||
<td>{{ val.from }}</td><td>-</td><td>{{ val.to }}</td>
|
||||
{% endif %}
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</table>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</li>
|
||||
<li>
|
||||
<a class="dropdown-toggle no-text-underline collapsed" data-toggle="collapse" href="#collapseTopLocal" role="button" aria-expanded="false" aria-controls="collapseTopLocal">
|
||||
<i class="fa fa-clock-o"></i> Reisebüro
|
||||
{% if(content.available.local.active) %}
|
||||
<span class="text-success">geöffnet</span> •
|
||||
{% else %}
|
||||
<span class="text-danger">geschlossen</span> •
|
||||
{% endif %}
|
||||
{{ content.available.local.content }}
|
||||
<i class="fa fa-caret-collapse"></i>
|
||||
</a>
|
||||
|
||||
<div class="dropdown-menu-infos collapse" id="collapseTopLocal">
|
||||
<div class="dropdown-menu-body">
|
||||
<div class="badge badge-default btn-block">
|
||||
{% if(content.available.local.active) %}
|
||||
<span class="text-success"><i class="fa fa-check-circle fa-lg"></i></span> Unsere Büro ist aktuell geöffnet.
|
||||
{% else %}
|
||||
<span class="text-danger"><i class="fa fa-times-circle fa-lg"></i></span> Unsere Büro ist aktuell geschlossen.
|
||||
{% endif %}
|
||||
</div>
|
||||
<table class="table table-condensed table-vertical-middle">
|
||||
{% for key, val in local %}
|
||||
<tr>
|
||||
<td class="text-left" style="width: 50%">{{ val.day }} <span class="text-muted pull-right"> {{ val.date }}</span></td>
|
||||
{% if val.active == 0 %}
|
||||
<td colspan="3">geschlossen</td>
|
||||
{% else %}
|
||||
<td>{{ val.from }}</td><td>-</td><td>{{ val.to }}</td>
|
||||
{% endif %}
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</li>
|
||||
<li>
|
||||
<a class="dropdown-toggle no-text-underline collapsed" data-toggle="collapse" href="#collapseTopContact" role="button" aria-expanded="false" aria-controls="collapseTopContact"><i class="fa fa-envelope"></i> Kontakt • Formular • Terminvereinbarung <i class="fa fa-caret-collapse"></i></a>
|
||||
<div class="dropdown-menu-infos collapse" id="collapseTopContact">
|
||||
<div class="dropdown-menu-body">
|
||||
{{ content.info.office_appointment | raw }}
|
||||
</div>
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
<div id="header" class="sticky clearfix">
|
||||
<!-- TOP NAV -->
|
||||
<header id="topNav">
|
||||
<div class="container-fluid">
|
||||
|
||||
<!-- Mobile Menu Button -->
|
||||
<button class="btn btn-mobile" data-toggle="collapse" data-target=".nav-main-collapse">
|
||||
<i class="fa fa-bars"></i> Menü
|
||||
</button>
|
||||
<button class="btn btn-primary btn-mobile-info myanimated my_fadein">
|
||||
<i class="fa fa-info"></i>
|
||||
</button>
|
||||
<!-- Logo -->
|
||||
<a class="logo" href="/">
|
||||
<img src="{{ asset('images/wlogo.png') }}" alt="Stern Tours">
|
||||
</a>
|
||||
|
||||
<div class="navbar-collapse nav-main-collapse collapse">
|
||||
<nav class="nav-main">
|
||||
<ul class="topMain nav nav-pills nav-main md-pull-left">
|
||||
<li class=" active"><!-- HOME -->
|
||||
<a href="/" title="Kulturreisen" itemprop="">
|
||||
<i class="fa fa-home fa-lg"></i> <span class="hidden-md hidden-lg">Kulturreisen</span>
|
||||
</a>
|
||||
|
||||
</li>
|
||||
{% for nav_page in nav_pages if nav_page.country is not empty %}
|
||||
{% if nav_page.showInNavi == 1 %}
|
||||
{# @var nav_page \AppBundle\Entity\Page #}
|
||||
<li class="dropdown">
|
||||
<a class="dropdown-toggle" href="{{ nav_page.urlPath }}">
|
||||
<i class="fa fa-star"></i> {{ nav_page.title|replace({'Reisen': ''}) }} <span class="hidden-md hidden-lg">Reisen</span>
|
||||
</a>
|
||||
|
||||
<ul class="dropdown-menu">
|
||||
<li>
|
||||
<h4><i class="fa fa-star"></i> <a href="{{ nav_page.urlPath }}" title="{{ nav_page.title }}">
|
||||
{{ nav_page.title|replace({'Reisen': ''}) }} Reisen
|
||||
</a></h4>
|
||||
</li>
|
||||
|
||||
{% for childnav_page in nav_page.children %}
|
||||
{# @var childnav_page \AppBundle\Entity\Page #}
|
||||
|
||||
{% if(childnav_page.beforeTitle == "Infos") %}
|
||||
<li>
|
||||
<h4><i class="fa fa-info-circle"></i> Infos</h4>
|
||||
</li>
|
||||
{% endif %}
|
||||
<li role="menuitem" itemprop="name">
|
||||
<a itemprop="url" href="{{ childnav_page.urlPath }}" title="{{ childnav_page.title }}">
|
||||
{{ childnav_page.titleShort }}
|
||||
</a>
|
||||
</li>
|
||||
|
||||
{% endfor %}
|
||||
</ul>
|
||||
</li>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
</ul>
|
||||
<ul class="topMain nav nav-pills nav-main md-pull-right">
|
||||
<li class="dropdown">
|
||||
<a class="dropdown-toggle extra-margin-left" href="/ferienwohnungen">
|
||||
<span class="text-usedom">USEDOM</span> <span class="hidden-md">Ferienwohnungen</span> <span class="hidden-sm hidden-lg hidden-xs">FeWo</span>
|
||||
</a>
|
||||
<ul class="dropdown-menu pull-right">
|
||||
<li role="menuitem" itemprop="name">
|
||||
<a itemprop="url" href="/ferienwohnungen" title="Usedom Ferienwohnungen"><i class="isv-fewo"></i> Übersicht </a>
|
||||
</li>
|
||||
<li class="divider"></li>
|
||||
<li role="menuitem" itemprop="name">
|
||||
<a itemprop="url" href="/ferienwohnungen/fewo1-strandstr29" title="FeWo 1 Strandstr. 29">
|
||||
<i class="isv-fewo"></i> FeWo 1 Strandstr. 29
|
||||
</a>
|
||||
</li>
|
||||
<li role="menuitem" itemprop="name">
|
||||
<a itemprop="url" href="/ferienwohnungen/fewo2-strandstr29" title="FeWo 2 Strandstr. 29">
|
||||
<i class="isv-fewo"></i> FeWo 2 Strandstr. 29
|
||||
</a>
|
||||
</li>
|
||||
<li role="menuitem" itemprop="name">
|
||||
<a itemprop="url" href="/ferienwohnungen/fewo3-strandstr29" title="FeWo 3 Strandstr. 29">
|
||||
<i class="isv-fewo"></i> FeWo 3 Strandstr. 29
|
||||
</a>
|
||||
</li>
|
||||
<li role="menuitem" itemprop="name">
|
||||
<a itemprop="url" href="/ferienwohnungen/fewo4-strandstr29" title="FeWo 4 Strandstr. 29">
|
||||
<i class="isv-fewo"></i> FeWo 4 Strandstr. 29
|
||||
</a>
|
||||
</li>
|
||||
<li role="menuitem" itemprop="name">
|
||||
<a itemprop="url" href="/ferienwohnungen/fewo1-triftweg10" title="FeWo 1 Triftweg 10">
|
||||
<i class="isv-fewo"></i> FeWo 1 Triftweg 10
|
||||
</a>
|
||||
</li>
|
||||
<li role="menuitem" itemprop="name">
|
||||
<a itemprop="url" href="/ferienwohnungen/fewo2-triftweg10" title="FeWo 2 Triftweg 10">
|
||||
<i class="isv-fewo"></i> FeWo 2 Triftweg 10
|
||||
</a>
|
||||
</li>
|
||||
|
||||
<li role="menuitem" itemprop="name">
|
||||
<a itemprop="url" href="/ferienwohnungen/fewo3-triftweg10" title="FeWo 3 Triftweg 10">
|
||||
<i class="isv-fewo"></i> FeWo 3 Triftweg 10
|
||||
</a>
|
||||
</li>
|
||||
<li role="menuitem" itemprop="name">
|
||||
<a itemprop="url" href="/ferienwohnungen/fewo4-triftweg10" title="FeWo 4 Triftweg 10">
|
||||
<i class="isv-fewo"></i> FeWo 4 Triftweg 10
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</li>
|
||||
<li class="dropdown">
|
||||
<a class="dropdown-toggle" href="#">
|
||||
<i class="fa fa-ellipsis-v fa-lg"></i> <span class="hidden-md hidden-lg">mehr</span>
|
||||
</a>
|
||||
<ul class="dropdown-menu pull-right">
|
||||
<li role="menuitem" itemprop="name">
|
||||
<a itemprop="url" href="/tuerkei-reisen" title="Türkei Reisen"><i class="fa fa-star"></i> Türkei Reisen </a>
|
||||
</li>
|
||||
<li role="menuitem" itemprop="name">
|
||||
<a itemprop="url" href="/israel-reisen" title="Israel Reisen"><i class="fa fa-star"></i> Israel Reisen </a>
|
||||
</li>
|
||||
<!--
|
||||
<li role="menuitem" itemprop="name">
|
||||
<a itemprop="url" href="/usbekistan-reisen" title="Usbekistan Reisen"><i class="fa fa-star"></i> Usbekistan Reisen </a>
|
||||
</li>
|
||||
<li role="menuitem" itemprop="name">
|
||||
<a itemprop="url" href="/marokko-urlaub" title="Marokko Reisen"><i class="fa fa-star"></i> Marokko Reisen</a>
|
||||
</li>
|
||||
-->
|
||||
<li class="divider"></li>
|
||||
|
||||
<li role="menuitem" itemprop="name">
|
||||
<a itemprop="url" href="/ueber-uns" title="Über uns"><i class="fa fa-users"></i> Über uns</a>
|
||||
</li>
|
||||
<li class="divider"></li>
|
||||
<li role="menuitem" itemprop="name">
|
||||
<a itemprop="url" href="/reiseversicherung" title="Reiseversicherung">
|
||||
<i class="fa fa-shield"></i> Reiseversicherung
|
||||
</a>
|
||||
</li>
|
||||
<li role="menuitem" itemprop="name">
|
||||
<a itemprop="url" href="/reisefuehrer" title="Reiseführer">
|
||||
<i class="fa fa-flag"></i> Reiseführer
|
||||
</a>
|
||||
</li>
|
||||
<li role="menuitem" itemprop="name">
|
||||
<a itemprop="url" href="/reisemagazin" title="Reisemagazin">
|
||||
<i class="fa fa-book"></i> Reisemagazin
|
||||
</a>
|
||||
</li>
|
||||
<li role="menuitem" itemprop="name">
|
||||
<a itemprop="url" href="/reisenews" title="Reisenews">
|
||||
<i class="fa fa-newspaper-o"></i> Reisenews
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</li>
|
||||
</ul>
|
||||
</nav>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
</div>
|
||||
414
dev/frontend-navigation/navigation-api.md
Normal file
|
|
@ -0,0 +1,414 @@
|
|||
# Navigation API Dokumentation
|
||||
|
||||
Die Navigation-API stellt Endpunkte bereit, um die Frontend-Navigationsstruktur im Backend abzurufen.
|
||||
|
||||
## Basis-URL
|
||||
|
||||
Alle Endpunkte sind unter `/api/navigation/` verfügbar.
|
||||
|
||||
## Endpunkte
|
||||
|
||||
### 1. Kompletter Navigationsbaum
|
||||
|
||||
**GET** `/api/navigation/tree`
|
||||
|
||||
Gibt den kompletten hierarchischen Navigationsbaum mit allen Seiten zurück.
|
||||
|
||||
**Response:**
|
||||
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"data": [
|
||||
{
|
||||
"id": 1,
|
||||
"title": "Startseite",
|
||||
"title_short": null,
|
||||
"before_title": null,
|
||||
"slug": "startseite",
|
||||
"real_url_path": "/",
|
||||
"url": "/",
|
||||
"status": 1,
|
||||
"show_in_navi": 1,
|
||||
"order": 1,
|
||||
"template": "home",
|
||||
"lvl": 0,
|
||||
"parent_id": null,
|
||||
"pagetitle": "Willkommen",
|
||||
"description": "Startseite",
|
||||
"keywords": "start, home",
|
||||
"canonical_url": null,
|
||||
"lft": 1,
|
||||
"rgt": 10,
|
||||
"tree_root": 1,
|
||||
"box_body": null,
|
||||
"box_image_url": null,
|
||||
"box_star": null,
|
||||
"box_discount": null,
|
||||
"is_travel_program": false,
|
||||
"is_fewo_lodging": false,
|
||||
"is_country_page": false,
|
||||
"cms_settings": null,
|
||||
"children": [
|
||||
{
|
||||
"id": 2,
|
||||
"title": "Über uns",
|
||||
"slug": "ueber-uns",
|
||||
"url": "/ueber-uns",
|
||||
"children": [],
|
||||
"has_children": false
|
||||
}
|
||||
],
|
||||
"has_children": true
|
||||
}
|
||||
],
|
||||
"meta": {
|
||||
"total_nodes": 42,
|
||||
"generated_at": "2024-01-15T10:30:00+00:00"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 2. Nur aktive Navigationspunkte
|
||||
|
||||
**GET** `/api/navigation/tree/active`
|
||||
|
||||
Gibt nur aktive Seiten zurück (`status = 1` und `show_in_navi = 1`).
|
||||
|
||||
**Response:** Gleiche Struktur wie oben, aber gefiltert.
|
||||
|
||||
---
|
||||
|
||||
### 3. Teilbaum ab einem bestimmten Knoten
|
||||
|
||||
**GET** `/api/navigation/tree/{rootId}`
|
||||
|
||||
Gibt einen Teilbaum zurück, beginnend mit der angegebenen Page-ID.
|
||||
|
||||
**Parameter:**
|
||||
|
||||
- `rootId` (integer): Die ID der Seite, ab der der Baum aufgebaut werden soll
|
||||
|
||||
**Beispiel:** `/api/navigation/tree/5`
|
||||
|
||||
**Response:**
|
||||
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"data": {
|
||||
"id": 5,
|
||||
"title": "Reisen",
|
||||
"slug": "reisen",
|
||||
"url": "/reisen",
|
||||
"children": [
|
||||
{
|
||||
"id": 6,
|
||||
"title": "Türkei",
|
||||
"slug": "tuerkei",
|
||||
"url": "/reisen/tuerkei",
|
||||
"children": [],
|
||||
"has_children": false
|
||||
}
|
||||
],
|
||||
"has_children": true
|
||||
},
|
||||
"meta": {
|
||||
"total_nodes": 15,
|
||||
"generated_at": "2024-01-15T10:30:00+00:00"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 4. Flache Liste (ohne Hierarchie)
|
||||
|
||||
**GET** `/api/navigation/flat`
|
||||
|
||||
Gibt alle Navigationspunkte als flache Liste zurück (ohne parent-child Beziehung).
|
||||
|
||||
**Response:**
|
||||
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"data": [
|
||||
{
|
||||
"id": 1,
|
||||
"title": "Startseite",
|
||||
"slug": "startseite",
|
||||
"url": "/",
|
||||
"status": 1,
|
||||
"show_in_navi": 1,
|
||||
"order": 1,
|
||||
"template": "home",
|
||||
"lvl": 0,
|
||||
"parent_id": null,
|
||||
"is_travel_program": false,
|
||||
"is_fewo_lodging": false,
|
||||
"is_country_page": false
|
||||
},
|
||||
{
|
||||
"id": 2,
|
||||
"title": "Über uns",
|
||||
"slug": "ueber-uns",
|
||||
"url": "/ueber-uns",
|
||||
"status": 1,
|
||||
"show_in_navi": 1,
|
||||
"order": 2,
|
||||
"template": "default",
|
||||
"lvl": 0,
|
||||
"parent_id": null,
|
||||
"is_travel_program": false,
|
||||
"is_fewo_lodging": false,
|
||||
"is_country_page": false
|
||||
}
|
||||
],
|
||||
"meta": {
|
||||
"total_nodes": 42,
|
||||
"generated_at": "2024-01-15T10:30:00+00:00"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 5. Breadcrumb-Pfad
|
||||
|
||||
**GET** `/api/navigation/breadcrumb/{pageId}`
|
||||
|
||||
Gibt den Breadcrumb-Pfad für eine bestimmte Seite zurück (von der Wurzel bis zur Zielseite).
|
||||
|
||||
**Parameter:**
|
||||
|
||||
- `pageId` (integer): Die ID der Seite, für die der Breadcrumb erstellt werden soll
|
||||
|
||||
**Beispiel:** `/api/navigation/breadcrumb/15`
|
||||
|
||||
**Response:**
|
||||
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"data": [
|
||||
{
|
||||
"id": 1,
|
||||
"title": "Startseite",
|
||||
"title_short": null,
|
||||
"url": "/",
|
||||
"slug": "startseite"
|
||||
},
|
||||
{
|
||||
"id": 5,
|
||||
"title": "Reisen",
|
||||
"title_short": null,
|
||||
"url": "/reisen",
|
||||
"slug": "reisen"
|
||||
},
|
||||
{
|
||||
"id": 10,
|
||||
"title": "Türkei",
|
||||
"title_short": null,
|
||||
"url": "/reisen/tuerkei",
|
||||
"slug": "tuerkei"
|
||||
},
|
||||
{
|
||||
"id": 15,
|
||||
"title": "Istanbul",
|
||||
"title_short": null,
|
||||
"url": "/reisen/tuerkei/istanbul",
|
||||
"slug": "istanbul"
|
||||
}
|
||||
],
|
||||
"meta": {
|
||||
"depth": 4,
|
||||
"generated_at": "2024-01-15T10:30:00+00:00"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Felder-Beschreibung
|
||||
|
||||
Jeder Navigationspunkt enthält folgende Informationen:
|
||||
|
||||
### Basis-Informationen
|
||||
|
||||
- `id`: Eindeutige ID der Seite
|
||||
- `title`: Vollständiger Titel
|
||||
- `title_short`: Kurzer Titel (optional)
|
||||
- `before_title`: Text vor dem Titel (optional)
|
||||
- `slug`: URL-freundlicher Name
|
||||
- `real_url_path`: Der definierte URL-Pfad aus der Datenbank
|
||||
- `url`: Der vollständige URL-Pfad (entweder `real_url_path` oder automatisch generiert)
|
||||
|
||||
### Navigationsinformationen
|
||||
|
||||
- `status`: Status der Seite (0 = inaktiv, 1 = aktiv)
|
||||
- `show_in_navi`: Soll in Navigation angezeigt werden (0 = nein, 1 = ja)
|
||||
- `order`: Sortierreihenfolge
|
||||
- `lvl`: Hierarchie-Ebene (0 = Root)
|
||||
- `parent_id`: ID der Eltern-Seite (null = Root)
|
||||
|
||||
### SEO-Informationen
|
||||
|
||||
- `pagetitle`: SEO-Titel
|
||||
- `description`: Meta-Beschreibung
|
||||
- `keywords`: Meta-Keywords
|
||||
- `canonical_url`: Kanonische URL
|
||||
|
||||
### Template & Layout
|
||||
|
||||
- `template`: Template-Name (z.B. "home", "overview", "default")
|
||||
- `box_body`: Inhalt für Box-Darstellung
|
||||
- `box_image_url`: Bild-URL für Box
|
||||
- `box_star`: Sternebewertung
|
||||
- `box_discount`: Rabatt-Information
|
||||
|
||||
### Tree-Struktur
|
||||
|
||||
- `lft`, `rgt`: Nested Set Tree Werte
|
||||
- `tree_root`: ID der Baumwurzel
|
||||
|
||||
### Content-Type Flags
|
||||
|
||||
- `is_travel_program`: Ist ein Reiseprogramm
|
||||
- `is_fewo_lodging`: Ist eine Ferienwohnung
|
||||
- `is_country_page`: Ist eine Länderseite
|
||||
|
||||
### Beziehungen (wenn vorhanden)
|
||||
|
||||
**Travel Program:**
|
||||
|
||||
```json
|
||||
"travel_program": {
|
||||
"id": 123,
|
||||
"title": "7 Tage Istanbul",
|
||||
"subtitle": "Die schönsten Orte",
|
||||
"status": 1,
|
||||
"position": 1,
|
||||
"duration": 7,
|
||||
"price_from": 899.00
|
||||
}
|
||||
```
|
||||
|
||||
**Fewo Lodging:**
|
||||
|
||||
```json
|
||||
"fewo_lodging": {
|
||||
"id": 45,
|
||||
"name": "Villa am Meer",
|
||||
"status": 1
|
||||
}
|
||||
```
|
||||
|
||||
**Country:**
|
||||
|
||||
```json
|
||||
"country": {
|
||||
"id": 10,
|
||||
"name": "Türkei",
|
||||
"title": "Türkei Reisen",
|
||||
"slug": "tuerkei"
|
||||
}
|
||||
```
|
||||
|
||||
### Hierarchie-Informationen
|
||||
|
||||
- `children`: Array von Child-Knoten (gleiche Struktur)
|
||||
- `has_children`: Boolean, ob Kinder vorhanden sind
|
||||
|
||||
---
|
||||
|
||||
## Fehlerbehandlung
|
||||
|
||||
Bei Fehlern wird ein JSON-Response mit folgendem Format zurückgegeben:
|
||||
|
||||
```json
|
||||
{
|
||||
"success": false,
|
||||
"error": "Fehlerbeschreibung"
|
||||
}
|
||||
```
|
||||
|
||||
**HTTP Status Codes:**
|
||||
|
||||
- `200`: Erfolgreiche Anfrage
|
||||
- `404`: Seite/Ressource nicht gefunden
|
||||
- `500`: Serverfehler
|
||||
|
||||
---
|
||||
|
||||
## Verwendungsbeispiele
|
||||
|
||||
### JavaScript/Fetch
|
||||
|
||||
```javascript
|
||||
// Kompletten Navigationsbaum abrufen
|
||||
fetch("/api/navigation/tree")
|
||||
.then((response) => response.json())
|
||||
.then((data) => {
|
||||
console.log("Navigation Tree:", data.data);
|
||||
console.log("Total Nodes:", data.meta.total_nodes);
|
||||
});
|
||||
|
||||
// Breadcrumb für eine Seite abrufen
|
||||
fetch("/api/navigation/breadcrumb/15")
|
||||
.then((response) => response.json())
|
||||
.then((data) => {
|
||||
console.log("Breadcrumb:", data.data);
|
||||
});
|
||||
```
|
||||
|
||||
### PHP/Laravel
|
||||
|
||||
```php
|
||||
use Illuminate\Support\Facades\Http;
|
||||
|
||||
// Kompletten Navigationsbaum abrufen
|
||||
$response = Http::get('http://yourdomain.com/api/navigation/tree');
|
||||
$navigationTree = $response->json()['data'];
|
||||
|
||||
// Teilbaum abrufen
|
||||
$response = Http::get('http://yourdomain.com/api/navigation/tree/5');
|
||||
$subTree = $response->json()['data'];
|
||||
```
|
||||
|
||||
### cURL
|
||||
|
||||
```bash
|
||||
# Kompletten Baum
|
||||
curl -X GET http://yourdomain.com/api/navigation/tree
|
||||
|
||||
# Nur aktive Navigationspunkte
|
||||
curl -X GET http://yourdomain.com/api/navigation/tree/active
|
||||
|
||||
# Breadcrumb
|
||||
curl -X GET http://yourdomain.com/api/navigation/breadcrumb/15
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Performance-Hinweise
|
||||
|
||||
- Der komplette Navigationsbaum kann bei großen Websites viele Knoten enthalten
|
||||
- Verwenden Sie `/api/navigation/tree/active` für Frontend-Navigationen
|
||||
- Verwenden Sie `/api/navigation/tree/{rootId}` um nur relevante Teilbäume zu laden
|
||||
- Erwägen Sie Caching für häufig abgerufene Navigationsstrukturen
|
||||
|
||||
---
|
||||
|
||||
## Implementierung
|
||||
|
||||
Die Navigation-API basiert auf folgenden Komponenten:
|
||||
|
||||
1. **Controller:** `App\Http\Controllers\API\NavigationController`
|
||||
2. **Service:** `App\Services\NavigationTreeService`
|
||||
3. **Model:** `App\Models\Page`
|
||||
4. **Routen:** Definiert in `routes/api.php`
|
||||
|
||||
Der Service verwendet rekursive Methoden, um den hierarchischen Baum aufzubauen und kann einfach erweitert werden.
|
||||
47
dev/frontend-navigation/navigation.md
Normal file
|
|
@ -0,0 +1,47 @@
|
|||
# Routing & Navigations-Logik (KernelControllerListener)
|
||||
|
||||
Die Klasse `src/AppBundle/Listener/KernelControllerListener.php` implementiert die zentrale Routing-Logik für das CMS. Sie greift ein, wenn der Symfony-Router den Request an `AppBundle\Controller\DefaultController::defaultAction` leitet.
|
||||
|
||||
## Funktionsweise
|
||||
|
||||
Der Listener analysiert den Request-Pfad (`pathInfo`) und entscheidet, welcher Controller tatsächlich ausgeführt werden soll.
|
||||
|
||||
### 1. API-Lookup (Priorität 1)
|
||||
|
||||
Zuerst wird geprüft, ob der Pfad über einen externen API-Call (`Util::loadFromApi('cms/search', ...)`) aufgelöst werden kann.
|
||||
|
||||
- **Ziel-Controller:** `AppBundle:Cms:iqTravelGuide`
|
||||
- **Template:** `TravelGuide`
|
||||
|
||||
### 2. Datenbank-Suche (Page Entity)
|
||||
|
||||
Wenn die API nichts zurückgibt, wird in der lokalen Datenbank (`AppBundle:Page`) gesucht.
|
||||
|
||||
- **Methode A (Direkt):** Suche nach Übereinstimmung im Feld `realUrlPath`.
|
||||
- **Methode B (Tree-Traversierung):** Der Pfad wird anhand der Slashes (`/`) zerlegt. Es wird versucht, den Pfad hierarchisch über `slug` und die Eltern-Kind-Beziehung (`parent`) aufzulösen. Die anfällige Nested-Set-Logik (`lft`/`rgt`, `lvl`) wird hier bewusst umgangen, um 404-Fehler bei inkonsistenten Trees zu vermeiden.
|
||||
|
||||
### 3. Redirects
|
||||
|
||||
- **Hardcoded:** Es gibt eine Liste fester 301-Weiterleitungen (z.B. alte Reiseführer-URLs).
|
||||
- **Datenbank:** Tabelle `AppBundle:Redirect`. Wenn keine Page gefunden wird, wird hier geprüft.
|
||||
|
||||
### 4. Controller-Zuweisung
|
||||
|
||||
Sobald eine `Page`-Entität (`$node`) gefunden wurde, wird der Controller basierend auf dem Inhaltstyp bestimmt:
|
||||
|
||||
| Inhaltstyp | URL-Suffix | Controller | Action |
|
||||
| ----------------- | ------------------------- | ----------------------- | ------------------------- |
|
||||
| **TravelProgram** | `/buchen`, `/berechne...` | `AppBundle:Booking` | `index` |
|
||||
| **TravelProgram** | `/pdf` | `AppBundle:Cms` | `pdf` |
|
||||
| **TravelProgram** | _(keins)_ | `AppBundle:Cms` | `travelProgram` |
|
||||
| **FewoLodging** | `/buchen` | `AppBundle:FewoBooking` | `index` |
|
||||
| **FewoLodging** | _(keins)_ | `AppBundle:Cms` | `fewoLodging` |
|
||||
| **Standard Page** | _(keins)_ | `AppBundle:Cms` | _Dyn. nach Template-Name_ |
|
||||
|
||||
**Fallback für Standard Pages:**
|
||||
Wenn die Page ein Template (z.B. "About") definiert hat, versucht das System `AppBundle:Cms:About` aufzurufen. Existiert diese Action nicht, wird `AppBundle:Cms:Default` verwendet und der Template-Name als Parameter übergeben.
|
||||
|
||||
### 5. Fehlerbehandlung
|
||||
|
||||
- **404:** Wenn Pfad nicht gefunden und nicht in der Whitelist (`buchen`, `pdf` etc.) -> `HttpException(404)`.
|
||||
- **Inaktiv:** Wenn Page-Status `0` ist -> `NotFoundHttpException`.
|
||||
496
dev/frontend-navigation/test-api.html
Normal file
|
|
@ -0,0 +1,496 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="de">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Navigation API Tester</title>
|
||||
<style>
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, sans-serif;
|
||||
background: #f5f5f5;
|
||||
padding: 20px;
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
.container {
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
h1 {
|
||||
color: #333;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.intro {
|
||||
background: #fff;
|
||||
padding: 20px;
|
||||
border-radius: 8px;
|
||||
margin-bottom: 20px;
|
||||
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
|
||||
}
|
||||
|
||||
.config {
|
||||
background: #fff;
|
||||
padding: 20px;
|
||||
border-radius: 8px;
|
||||
margin-bottom: 20px;
|
||||
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
|
||||
}
|
||||
|
||||
.config input {
|
||||
width: 100%;
|
||||
padding: 10px;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 4px;
|
||||
font-size: 14px;
|
||||
margin-top: 5px;
|
||||
}
|
||||
|
||||
.endpoint-group {
|
||||
background: #fff;
|
||||
padding: 20px;
|
||||
border-radius: 8px;
|
||||
margin-bottom: 15px;
|
||||
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
|
||||
}
|
||||
|
||||
.endpoint-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
|
||||
.endpoint-title {
|
||||
font-size: 18px;
|
||||
font-weight: bold;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.endpoint-method {
|
||||
display: inline-block;
|
||||
padding: 4px 12px;
|
||||
border-radius: 4px;
|
||||
font-size: 12px;
|
||||
font-weight: bold;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.method-get {
|
||||
background: #61affe;
|
||||
}
|
||||
|
||||
.method-post {
|
||||
background: #49cc90;
|
||||
}
|
||||
|
||||
.endpoint-url {
|
||||
background: #f7f7f7;
|
||||
padding: 10px;
|
||||
border-radius: 4px;
|
||||
font-family: 'Courier New', monospace;
|
||||
font-size: 14px;
|
||||
margin-bottom: 15px;
|
||||
word-break: break-all;
|
||||
}
|
||||
|
||||
.endpoint-description {
|
||||
color: #666;
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
|
||||
.test-button {
|
||||
background: #4CAF50;
|
||||
color: white;
|
||||
border: none;
|
||||
padding: 10px 20px;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
font-size: 14px;
|
||||
transition: background 0.3s;
|
||||
}
|
||||
|
||||
.test-button:hover {
|
||||
background: #45a049;
|
||||
}
|
||||
|
||||
.test-button:disabled {
|
||||
background: #ccc;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.result {
|
||||
margin-top: 15px;
|
||||
padding: 15px;
|
||||
border-radius: 4px;
|
||||
display: none;
|
||||
}
|
||||
|
||||
.result.success {
|
||||
background: #d4edda;
|
||||
border: 1px solid #c3e6cb;
|
||||
color: #155724;
|
||||
}
|
||||
|
||||
.result.error {
|
||||
background: #f8d7da;
|
||||
border: 1px solid #f5c6cb;
|
||||
color: #721c24;
|
||||
}
|
||||
|
||||
.result-header {
|
||||
font-weight: bold;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.result-meta {
|
||||
margin-bottom: 10px;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.result-data {
|
||||
background: #f7f7f7;
|
||||
padding: 10px;
|
||||
border-radius: 4px;
|
||||
font-family: 'Courier New', monospace;
|
||||
font-size: 12px;
|
||||
max-height: 400px;
|
||||
overflow: auto;
|
||||
white-space: pre-wrap;
|
||||
word-wrap: break-word;
|
||||
}
|
||||
|
||||
.loading {
|
||||
display: inline-block;
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
border: 3px solid #f3f3f3;
|
||||
border-top: 3px solid #4CAF50;
|
||||
border-radius: 50%;
|
||||
animation: spin 1s linear infinite;
|
||||
margin-left: 10px;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
0% { transform: rotate(0deg); }
|
||||
100% { transform: rotate(360deg); }
|
||||
}
|
||||
|
||||
.test-all {
|
||||
background: #2196F3;
|
||||
color: white;
|
||||
border: none;
|
||||
padding: 15px 30px;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
font-size: 16px;
|
||||
margin-bottom: 20px;
|
||||
transition: background 0.3s;
|
||||
display: block;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.test-all:hover {
|
||||
background: #0b7dda;
|
||||
}
|
||||
|
||||
.summary {
|
||||
background: #fff;
|
||||
padding: 20px;
|
||||
border-radius: 8px;
|
||||
margin-top: 20px;
|
||||
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
|
||||
display: none;
|
||||
}
|
||||
|
||||
.summary h2 {
|
||||
margin-bottom: 15px;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.summary-stats {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
|
||||
gap: 15px;
|
||||
margin-top: 15px;
|
||||
}
|
||||
|
||||
.stat-card {
|
||||
padding: 15px;
|
||||
border-radius: 4px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.stat-card.total {
|
||||
background: #e3f2fd;
|
||||
}
|
||||
|
||||
.stat-card.success {
|
||||
background: #d4edda;
|
||||
}
|
||||
|
||||
.stat-card.error {
|
||||
background: #f8d7da;
|
||||
}
|
||||
|
||||
.stat-number {
|
||||
font-size: 32px;
|
||||
font-weight: bold;
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
|
||||
.stat-label {
|
||||
font-size: 14px;
|
||||
color: #666;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<h1>🗺️ Navigation API Tester</h1>
|
||||
|
||||
<div class="intro">
|
||||
<p>Dieses Tool testet alle verfügbaren Endpunkte der Navigation API und zeigt die Ergebnisse in einer strukturierten Form.</p>
|
||||
</div>
|
||||
|
||||
<div class="config">
|
||||
<label for="baseUrl"><strong>Basis-URL:</strong></label>
|
||||
<input type="text" id="baseUrl" value="http://localhost/api/navigation" placeholder="http://localhost/api/navigation">
|
||||
</div>
|
||||
|
||||
<button class="test-all" onclick="testAllEndpoints()">Alle Endpunkte testen</button>
|
||||
|
||||
<!-- Endpoint 1: Kompletter Navigationsbaum -->
|
||||
<div class="endpoint-group">
|
||||
<div class="endpoint-header">
|
||||
<div class="endpoint-title">Kompletter Navigationsbaum</div>
|
||||
<span class="endpoint-method method-get">GET</span>
|
||||
</div>
|
||||
<div class="endpoint-url">/tree</div>
|
||||
<div class="endpoint-description">
|
||||
Gibt den kompletten hierarchischen Navigationsbaum mit allen Seiten zurück.
|
||||
</div>
|
||||
<button class="test-button" onclick="testEndpoint('tree', 'GET')">Testen</button>
|
||||
<div class="result" id="result-tree"></div>
|
||||
</div>
|
||||
|
||||
<!-- Endpoint 2: Aktive Navigationspunkte -->
|
||||
<div class="endpoint-group">
|
||||
<div class="endpoint-header">
|
||||
<div class="endpoint-title">Nur aktive Navigationspunkte</div>
|
||||
<span class="endpoint-method method-get">GET</span>
|
||||
</div>
|
||||
<div class="endpoint-url">/tree/active</div>
|
||||
<div class="endpoint-description">
|
||||
Gibt nur aktive Seiten zurück (status = 1 und show_in_navi = 1).
|
||||
</div>
|
||||
<button class="test-button" onclick="testEndpoint('tree/active', 'GET')">Testen</button>
|
||||
<div class="result" id="result-tree-active"></div>
|
||||
</div>
|
||||
|
||||
<!-- Endpoint 3: Teilbaum -->
|
||||
<div class="endpoint-group">
|
||||
<div class="endpoint-header">
|
||||
<div class="endpoint-title">Teilbaum ab Root-ID</div>
|
||||
<span class="endpoint-method method-get">GET</span>
|
||||
</div>
|
||||
<div class="endpoint-url">/tree/{rootId}</div>
|
||||
<div class="endpoint-description">
|
||||
Gibt einen Teilbaum zurück, beginnend mit der angegebenen Page-ID.<br>
|
||||
<input type="number" id="subtreeId" placeholder="Root Page ID" value="1" style="margin-top: 10px; padding: 8px; width: 200px;">
|
||||
</div>
|
||||
<button class="test-button" onclick="testEndpoint('tree/' + document.getElementById('subtreeId').value, 'GET')">Testen</button>
|
||||
<div class="result" id="result-tree-subtree"></div>
|
||||
</div>
|
||||
|
||||
<!-- Endpoint 4: Flache Liste -->
|
||||
<div class="endpoint-group">
|
||||
<div class="endpoint-header">
|
||||
<div class="endpoint-title">Flache Liste</div>
|
||||
<span class="endpoint-method method-get">GET</span>
|
||||
</div>
|
||||
<div class="endpoint-url">/flat</div>
|
||||
<div class="endpoint-description">
|
||||
Gibt alle Navigationspunkte als flache Liste zurück (ohne parent-child Beziehung).
|
||||
</div>
|
||||
<button class="test-button" onclick="testEndpoint('flat', 'GET')">Testen</button>
|
||||
<div class="result" id="result-flat"></div>
|
||||
</div>
|
||||
|
||||
<!-- Endpoint 5: Breadcrumb -->
|
||||
<div class="endpoint-group">
|
||||
<div class="endpoint-header">
|
||||
<div class="endpoint-title">Breadcrumb-Pfad</div>
|
||||
<span class="endpoint-method method-get">GET</span>
|
||||
</div>
|
||||
<div class="endpoint-url">/breadcrumb/{pageId}</div>
|
||||
<div class="endpoint-description">
|
||||
Gibt den Breadcrumb-Pfad für eine bestimmte Seite zurück.<br>
|
||||
<input type="number" id="breadcrumbId" placeholder="Page ID" value="1" style="margin-top: 10px; padding: 8px; width: 200px;">
|
||||
</div>
|
||||
<button class="test-button" onclick="testEndpoint('breadcrumb/' + document.getElementById('breadcrumbId').value, 'GET')">Testen</button>
|
||||
<div class="result" id="result-breadcrumb"></div>
|
||||
</div>
|
||||
|
||||
<!-- Endpoint 6: Cache leeren -->
|
||||
<div class="endpoint-group">
|
||||
<div class="endpoint-header">
|
||||
<div class="endpoint-title">Cache leeren</div>
|
||||
<span class="endpoint-method method-post">POST</span>
|
||||
</div>
|
||||
<div class="endpoint-url">/cache/clear</div>
|
||||
<div class="endpoint-description">
|
||||
Löscht den kompletten Navigation-Cache.
|
||||
</div>
|
||||
<button class="test-button" onclick="testEndpoint('cache/clear', 'POST')">Testen</button>
|
||||
<div class="result" id="result-cache-clear"></div>
|
||||
</div>
|
||||
|
||||
<!-- Zusammenfassung -->
|
||||
<div class="summary" id="summary">
|
||||
<h2>Test-Zusammenfassung</h2>
|
||||
<div class="summary-stats">
|
||||
<div class="stat-card total">
|
||||
<div class="stat-number" id="stat-total">0</div>
|
||||
<div class="stat-label">Gesamt</div>
|
||||
</div>
|
||||
<div class="stat-card success">
|
||||
<div class="stat-number" id="stat-success">0</div>
|
||||
<div class="stat-label">Erfolgreich</div>
|
||||
</div>
|
||||
<div class="stat-card error">
|
||||
<div class="stat-number" id="stat-error">0</div>
|
||||
<div class="stat-label">Fehlgeschlagen</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
let testResults = {};
|
||||
|
||||
function getBaseUrl() {
|
||||
return document.getElementById('baseUrl').value.replace(/\/$/, '');
|
||||
}
|
||||
|
||||
async function testEndpoint(endpoint, method = 'GET') {
|
||||
const resultId = 'result-' + endpoint.replace(/\//g, '-').replace(/\d+/g, 'subtree');
|
||||
const resultDiv = document.getElementById(resultId);
|
||||
|
||||
resultDiv.style.display = 'block';
|
||||
resultDiv.className = 'result';
|
||||
resultDiv.innerHTML = '<div class="result-header">Teste...<span class="loading"></span></div>';
|
||||
|
||||
const url = getBaseUrl() + '/' + endpoint;
|
||||
|
||||
try {
|
||||
const startTime = performance.now();
|
||||
const response = await fetch(url, {
|
||||
method: method,
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Accept': 'application/json'
|
||||
}
|
||||
});
|
||||
const endTime = performance.now();
|
||||
const duration = Math.round(endTime - startTime);
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
testResults[endpoint] = response.ok;
|
||||
|
||||
if (response.ok) {
|
||||
resultDiv.className = 'result success';
|
||||
let html = '<div class="result-header">✓ Erfolgreich</div>';
|
||||
html += '<div class="result-meta">';
|
||||
html += `<strong>HTTP Status:</strong> ${response.status}<br>`;
|
||||
html += `<strong>Dauer:</strong> ${duration}ms<br>`;
|
||||
|
||||
if (data.meta) {
|
||||
html += '<strong>Metadaten:</strong><br>';
|
||||
for (const [key, value] of Object.entries(data.meta)) {
|
||||
html += ` ${key}: ${value}<br>`;
|
||||
}
|
||||
}
|
||||
|
||||
html += '</div>';
|
||||
|
||||
if (data.data) {
|
||||
html += '<div class="result-data">';
|
||||
html += JSON.stringify(data.data, null, 2);
|
||||
html += '</div>';
|
||||
}
|
||||
|
||||
resultDiv.innerHTML = html;
|
||||
} else {
|
||||
resultDiv.className = 'result error';
|
||||
let html = '<div class="result-header">✗ Fehler</div>';
|
||||
html += '<div class="result-meta">';
|
||||
html += `<strong>HTTP Status:</strong> ${response.status}<br>`;
|
||||
html += `<strong>Dauer:</strong> ${duration}ms<br>`;
|
||||
if (data.error) {
|
||||
html += `<strong>Fehlermeldung:</strong> ${data.error}`;
|
||||
}
|
||||
html += '</div>';
|
||||
resultDiv.innerHTML = html;
|
||||
}
|
||||
} catch (error) {
|
||||
testResults[endpoint] = false;
|
||||
resultDiv.className = 'result error';
|
||||
resultDiv.innerHTML = `
|
||||
<div class="result-header">✗ Fehler</div>
|
||||
<div class="result-meta">
|
||||
<strong>Fehlermeldung:</strong> ${error.message}<br>
|
||||
<br>
|
||||
Mögliche Ursachen:<br>
|
||||
• Server läuft nicht<br>
|
||||
• CORS-Probleme<br>
|
||||
• Falsche Base-URL<br>
|
||||
• Netzwerkfehler
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
async function testAllEndpoints() {
|
||||
testResults = {};
|
||||
|
||||
const endpoints = [
|
||||
{ path: 'tree', method: 'GET' },
|
||||
{ path: 'tree/active', method: 'GET' },
|
||||
{ path: 'tree/1', method: 'GET' },
|
||||
{ path: 'flat', method: 'GET' },
|
||||
{ path: 'breadcrumb/1', method: 'GET' },
|
||||
{ path: 'cache/clear', method: 'POST' }
|
||||
];
|
||||
|
||||
for (const endpoint of endpoints) {
|
||||
await testEndpoint(endpoint.path, endpoint.method);
|
||||
await new Promise(resolve => setTimeout(resolve, 500)); // Kurze Pause zwischen Tests
|
||||
}
|
||||
|
||||
updateSummary();
|
||||
}
|
||||
|
||||
function updateSummary() {
|
||||
const total = Object.keys(testResults).length;
|
||||
const success = Object.values(testResults).filter(r => r === true).length;
|
||||
const error = total - success;
|
||||
|
||||
document.getElementById('stat-total').textContent = total;
|
||||
document.getElementById('stat-success').textContent = success;
|
||||
document.getElementById('stat-error').textContent = error;
|
||||
document.getElementById('summary').style.display = 'block';
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
249
dev/frontend-navigation/test-api.php
Normal file
|
|
@ -0,0 +1,249 @@
|
|||
<?php
|
||||
|
||||
/**
|
||||
* Test-Script für die Navigation API
|
||||
*
|
||||
* Dieses Script testet alle verfügbaren Endpunkte der Navigation API
|
||||
* und zeigt die Ergebnisse in einer strukturierten Form.
|
||||
*
|
||||
* Verwendung:
|
||||
* 1. Passe die BASE_URL an deine Umgebung an
|
||||
* 2. Führe aus: php test-api.php
|
||||
*/
|
||||
|
||||
// Konfiguration
|
||||
define('BASE_URL', 'http://localhost'); // Passe dies an deine Umgebung an
|
||||
define('API_PREFIX', '/api/navigation');
|
||||
|
||||
// Terminal Farben
|
||||
define('COLOR_GREEN', "\033[0;32m");
|
||||
define('COLOR_RED', "\033[0;31m");
|
||||
define('COLOR_YELLOW', "\033[0;33m");
|
||||
define('COLOR_BLUE', "\033[0;34m");
|
||||
define('COLOR_RESET', "\033[0m");
|
||||
|
||||
/**
|
||||
* Führt einen API-Request aus
|
||||
*/
|
||||
function apiRequest($endpoint, $method = 'GET')
|
||||
{
|
||||
$url = BASE_URL . API_PREFIX . $endpoint;
|
||||
|
||||
$ch = curl_init();
|
||||
curl_setopt($ch, CURLOPT_URL, $url);
|
||||
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
|
||||
curl_setopt($ch, CURLOPT_CUSTOMREQUEST, $method);
|
||||
curl_setopt($ch, CURLOPT_TIMEOUT, 30);
|
||||
|
||||
$startTime = microtime(true);
|
||||
$response = curl_exec($ch);
|
||||
$endTime = microtime(true);
|
||||
$duration = round(($endTime - $startTime) * 1000, 2);
|
||||
|
||||
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
|
||||
$error = curl_error($ch);
|
||||
curl_close($ch);
|
||||
|
||||
if ($error) {
|
||||
return [
|
||||
'success' => false,
|
||||
'error' => $error,
|
||||
'duration' => $duration
|
||||
];
|
||||
}
|
||||
|
||||
return [
|
||||
'success' => true,
|
||||
'http_code' => $httpCode,
|
||||
'data' => json_decode($response, true),
|
||||
'duration' => $duration
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Ausgabe-Helfer
|
||||
*/
|
||||
function printHeader($text)
|
||||
{
|
||||
echo "\n" . COLOR_BLUE . str_repeat('=', 80) . COLOR_RESET . "\n";
|
||||
echo COLOR_BLUE . $text . COLOR_RESET . "\n";
|
||||
echo COLOR_BLUE . str_repeat('=', 80) . COLOR_RESET . "\n\n";
|
||||
}
|
||||
|
||||
function printSuccess($text)
|
||||
{
|
||||
echo COLOR_GREEN . "✓ " . $text . COLOR_RESET . "\n";
|
||||
}
|
||||
|
||||
function printError($text)
|
||||
{
|
||||
echo COLOR_RED . "✗ " . $text . COLOR_RESET . "\n";
|
||||
}
|
||||
|
||||
function printInfo($text)
|
||||
{
|
||||
echo COLOR_YELLOW . "ℹ " . $text . COLOR_RESET . "\n";
|
||||
}
|
||||
|
||||
function printJson($data, $maxDepth = 3, $currentDepth = 0)
|
||||
{
|
||||
if ($currentDepth >= $maxDepth) {
|
||||
echo "[... gekürzt nach Tiefe $maxDepth]\n";
|
||||
return;
|
||||
}
|
||||
|
||||
echo json_encode($data, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES) . "\n";
|
||||
}
|
||||
|
||||
/**
|
||||
* Testet einen einzelnen Endpunkt
|
||||
*/
|
||||
function testEndpoint($name, $endpoint, $method = 'GET', $showFullData = false)
|
||||
{
|
||||
printInfo("Teste: $name");
|
||||
echo " Endpunkt: $endpoint\n";
|
||||
echo " Methode: $method\n";
|
||||
|
||||
$result = apiRequest($endpoint, $method);
|
||||
|
||||
if (!$result['success']) {
|
||||
printError("Request fehlgeschlagen: " . $result['error']);
|
||||
return false;
|
||||
}
|
||||
|
||||
$httpCode = $result['http_code'];
|
||||
$data = $result['data'];
|
||||
$duration = $result['duration'];
|
||||
|
||||
echo " HTTP Status: $httpCode\n";
|
||||
echo " Dauer: {$duration}ms\n";
|
||||
|
||||
if ($httpCode === 200) {
|
||||
printSuccess("Erfolgreich");
|
||||
|
||||
if (isset($data['success']) && $data['success']) {
|
||||
if (isset($data['meta'])) {
|
||||
echo "\n Metadaten:\n";
|
||||
foreach ($data['meta'] as $key => $value) {
|
||||
echo " $key: $value\n";
|
||||
}
|
||||
}
|
||||
|
||||
if ($showFullData && isset($data['data'])) {
|
||||
echo "\n Daten (gekürzt):\n";
|
||||
$preview = $data['data'];
|
||||
if (is_array($preview) && count($preview) > 2) {
|
||||
$preview = array_slice($preview, 0, 2);
|
||||
echo " " . json_encode($preview, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE) . "\n";
|
||||
echo " ... und " . (count($data['data']) - 2) . " weitere Einträge\n";
|
||||
} else {
|
||||
printJson($preview, 2);
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
printError("HTTP-Fehler $httpCode");
|
||||
if (isset($data['error'])) {
|
||||
echo " Fehlermeldung: " . $data['error'] . "\n";
|
||||
}
|
||||
}
|
||||
|
||||
echo "\n";
|
||||
return $httpCode === 200;
|
||||
}
|
||||
|
||||
// Hauptprogramm
|
||||
printHeader("Navigation API Test Suite");
|
||||
|
||||
echo "Base URL: " . BASE_URL . "\n";
|
||||
echo "API Prefix: " . API_PREFIX . "\n\n";
|
||||
|
||||
$results = [];
|
||||
|
||||
// Test 1: Kompletter Navigationsbaum
|
||||
printHeader("Test 1: Kompletter Navigationsbaum");
|
||||
$results['tree'] = testEndpoint(
|
||||
"Kompletter Navigationsbaum",
|
||||
"/tree",
|
||||
"GET",
|
||||
true
|
||||
);
|
||||
|
||||
// Test 2: Nur aktive Navigationspunkte
|
||||
printHeader("Test 2: Nur aktive Navigationspunkte");
|
||||
$results['tree_active'] = testEndpoint(
|
||||
"Aktive Navigationspunkte",
|
||||
"/tree/active",
|
||||
"GET",
|
||||
true
|
||||
);
|
||||
|
||||
// Test 3: Teilbaum (ersetze 1 mit einer existierenden Page-ID)
|
||||
printHeader("Test 3: Teilbaum ab Root-ID 1");
|
||||
$results['subtree'] = testEndpoint(
|
||||
"Teilbaum ab Root-ID 1",
|
||||
"/tree/1",
|
||||
"GET",
|
||||
true
|
||||
);
|
||||
|
||||
// Test 4: Flache Liste
|
||||
printHeader("Test 4: Flache Liste aller Navigationspunkte");
|
||||
$results['flat'] = testEndpoint(
|
||||
"Flache Liste",
|
||||
"/flat",
|
||||
"GET",
|
||||
true
|
||||
);
|
||||
|
||||
// Test 5: Breadcrumb (ersetze 1 mit einer existierenden Page-ID)
|
||||
printHeader("Test 5: Breadcrumb für Page-ID 1");
|
||||
$results['breadcrumb'] = testEndpoint(
|
||||
"Breadcrumb für Page-ID 1",
|
||||
"/breadcrumb/1",
|
||||
"GET",
|
||||
true
|
||||
);
|
||||
|
||||
// Test 6: Cache leeren
|
||||
printHeader("Test 6: Cache leeren");
|
||||
$results['cache_clear'] = testEndpoint(
|
||||
"Cache leeren",
|
||||
"/cache/clear",
|
||||
"POST",
|
||||
false
|
||||
);
|
||||
|
||||
// Zusammenfassung
|
||||
printHeader("Test-Zusammenfassung");
|
||||
|
||||
$total = count($results);
|
||||
$passed = count(array_filter($results));
|
||||
$failed = $total - $passed;
|
||||
|
||||
echo "Gesamt: $total Tests\n";
|
||||
printSuccess("Erfolgreich: $passed");
|
||||
if ($failed > 0) {
|
||||
printError("Fehlgeschlagen: $failed");
|
||||
}
|
||||
|
||||
$percentage = round(($passed / $total) * 100, 1);
|
||||
echo "\nErfolgsrate: $percentage%\n";
|
||||
|
||||
if ($percentage === 100.0) {
|
||||
printSuccess("\nAlle Tests bestanden! 🎉");
|
||||
} else {
|
||||
printError("\nEinige Tests sind fehlgeschlagen. Bitte überprüfen Sie die Ausgabe oben.");
|
||||
}
|
||||
|
||||
echo "\n";
|
||||
|
||||
// Weitere Hinweise
|
||||
printHeader("Weitere Informationen");
|
||||
echo "Vollständige API-Dokumentation: dev/frontend-navigation/navigation-api.md\n";
|
||||
echo "README: dev/frontend-navigation/README.md\n\n";
|
||||
echo "Hinweis: Wenn Tests fehlschlagen, überprüfen Sie:\n";
|
||||
echo " 1. Ist die BASE_URL korrekt?\n";
|
||||
echo " 2. Läuft der Server?\n";
|
||||
echo " 3. Sind Page-Einträge in der Datenbank vorhanden?\n";
|
||||
echo " 4. Sind die Routen korrekt registriert?\n\n";
|
||||
|
|
@ -1,22 +1,37 @@
|
|||
services:
|
||||
laravel.test:
|
||||
build:
|
||||
context: ./vendor/laravel/sail/runtimes/8.2
|
||||
context: ./vendor/laravel/sail/runtimes/8.3
|
||||
dockerfile: Dockerfile
|
||||
args:
|
||||
WWWGROUP: '${WWWGROUP}'
|
||||
image: sail-8.2/app
|
||||
WWWGROUP: '${WWWGROUP:-20}'
|
||||
WWWUSER: '${WWWUSER:-501}'
|
||||
image: sail-8.3/app
|
||||
extra_hosts:
|
||||
- 'host.docker.internal:host-gateway'
|
||||
ports:
|
||||
#- '${APP_PORT:-80}:80'
|
||||
- '${VITE_PORT:-5173}:${VITE_PORT:-5173}'
|
||||
|
||||
environment:
|
||||
WWWUSER: '${WWWUSER}'
|
||||
WWWUSER: '${WWWUSER:-501}'
|
||||
WWWGROUP: '${WWWGROUP:-20}'
|
||||
LARAVEL_SAIL: 1
|
||||
XDEBUG_MODE: '${SAIL_XDEBUG_MODE:-off}'
|
||||
XDEBUG_CONFIG: '${SAIL_XDEBUG_CONFIG:-client_host=host.docker.internal}'
|
||||
IGNITION_LOCAL_SITES_PATH: '${PWD}'
|
||||
DB_CONNECTION: mysql
|
||||
DB_HOST: mysql
|
||||
DB_PORT: 3306
|
||||
DB_DATABASE: stern_crm
|
||||
DB_USERNAME: sail
|
||||
DB_PASSWORD: password
|
||||
DB_CONNECTION_STERN: mysql
|
||||
DB_HOST_STERN: mysql-stern
|
||||
DB_PORT_STERN: 3306
|
||||
DB_DATABASE_STERN: stern_db
|
||||
DB_USERNAME_STERN: sail
|
||||
DB_PASSWORD_STERN: password
|
||||
MAIL_HOST: mailpit
|
||||
MAIL_PORT: 1025
|
||||
REDIS_HOST: redis
|
||||
volumes:
|
||||
- '.:/var/www/html'
|
||||
networks:
|
||||
|
|
@ -26,15 +41,31 @@ services:
|
|||
- mysql
|
||||
- mysql-stern
|
||||
- redis
|
||||
- mailpit
|
||||
labels:
|
||||
- "traefik.enable=true"
|
||||
# Hauptdomain
|
||||
- "traefik.http.routers.meinsterntours.rule=Host(`mein.sterntours.test`)"
|
||||
- "traefik.http.routers.meinsterntours.entrypoints=websecure"
|
||||
- "traefik.http.routers.meinsterntours.tls=true"
|
||||
- "traefik.http.routers.meinsterntours.service=meinsterntours-service"
|
||||
- "traefik.http.routers.meinsterntours.service=sterntours-service"
|
||||
|
||||
# Hauptdomain
|
||||
- "traefik.http.routers.sterntours.rule=Host(`sterntours.test`)"
|
||||
- "traefik.http.routers.sterntours.entrypoints=websecure"
|
||||
- "traefik.http.routers.sterntours.tls=true"
|
||||
- "traefik.http.routers.sterntours.service=sterntours-service"
|
||||
|
||||
# Asset Domain für Vite-Server (Port 5173 - Haupt/Portal)
|
||||
- "traefik.http.routers.assets-sterntours.rule=Host(`assets.sterntours.test`)"
|
||||
- "traefik.http.routers.assets-sterntours.entrypoints=websecure"
|
||||
- "traefik.http.routers.assets-sterntours.tls=true"
|
||||
- "traefik.http.routers.assets-sterntours.service=assets-sterntours-service"
|
||||
|
||||
# Service Definition - NUR EINMAL!
|
||||
- "traefik.http.services.meinsterntours-service.loadbalancer.server.port=80"
|
||||
- "traefik.http.services.sterntours-service.loadbalancer.server.port=80"
|
||||
- "traefik.http.services.assets-sterntours-service.loadbalancer.server.port=5173"
|
||||
- "traefik.http.services.assets-sterntours-service.loadbalancer.server.scheme=http"
|
||||
- "traefik.docker.network=proxy"
|
||||
mysql:
|
||||
image: 'mysql/mysql-server:8.0'
|
||||
|
|
@ -99,6 +130,21 @@ services:
|
|||
- ping
|
||||
retries: 3
|
||||
timeout: 5s
|
||||
mailpit:
|
||||
image: 'axllent/mailpit:latest'
|
||||
ports:
|
||||
- '${FORWARD_MAILPIT_PORT:-1030}:1025'
|
||||
- '${FORWARD_MAILPIT_DASHBOARD_PORT:-8030}:8025'
|
||||
networks:
|
||||
- sail
|
||||
- proxy
|
||||
labels:
|
||||
- "traefik.enable=true"
|
||||
- "traefik.http.routers.sterntours-mail.rule=Host(`sterntours-mail.test`)"
|
||||
- "traefik.http.routers.sterntours-mail.entrypoints=websecure"
|
||||
- "traefik.http.routers.sterntours-mail.tls=true"
|
||||
- "traefik.http.services.sterntours-mail.loadbalancer.server.port=8025"
|
||||
- "traefik.docker.network=proxy"
|
||||
networks:
|
||||
sail:
|
||||
driver: bridge
|
||||
|
|
|
|||
324
docs/NEWSLETTER.md
Normal file
|
|
@ -0,0 +1,324 @@
|
|||
# Newsletter-Modul Dokumentation
|
||||
|
||||
## Übersicht
|
||||
|
||||
Das Newsletter-Modul ermöglicht die zentrale Verwaltung von Newsletter-Kontakten aus beiden Buchungssystemen (Kulturreisen und Ferienwohnungen). Es synchronisiert automatisch Kundendaten aus Buchungen und bietet Export-Funktionen für Newsletter-Kampagnen.
|
||||
|
||||
## Features
|
||||
|
||||
- ✅ Zentrale Kontaktverwaltung für beide Newsletter-Gruppen
|
||||
- ✅ Automatische Synchronisation aus Buchungssystemen
|
||||
- ✅ Duplikat-Erkennung über E-Mail-Adressen
|
||||
- ✅ Tracking von Buchungsstatistiken
|
||||
- ✅ Status-Verwaltung (Aktiv, Inaktiv, Abgemeldet, Bounced)
|
||||
- ✅ Herkunfts-Tracking (Buchung, Newsletter-Anmeldung, etc.)
|
||||
- ✅ Export-Funktion (CSV)
|
||||
- ✅ Aktivitäts-Log für jeden Kontakt
|
||||
- ✅ Filter- und Suchfunktionen
|
||||
|
||||
## Installation
|
||||
|
||||
### 1. Migrationen ausführen
|
||||
|
||||
```bash
|
||||
./vendor/bin/sail artisan migrate
|
||||
```
|
||||
|
||||
Dies erstellt folgende Tabellen:
|
||||
- `newsletter_contacts` - Haupt-Kontakte-Tabelle
|
||||
- `newsletter_logs` - Aktivitäts-Log
|
||||
|
||||
### 2. Berechtigung hinzufügen
|
||||
|
||||
In der Datenbank muss die Berechtigung `cms-newsletter` für Benutzer aktiviert werden, die Zugriff auf das Newsletter-Modul haben sollen.
|
||||
|
||||
```sql
|
||||
-- Beispiel: Berechtigung für einen User aktivieren
|
||||
-- Dies muss über das bestehende Berechtigungssystem erfolgen
|
||||
```
|
||||
|
||||
### 3. Erste Synchronisation
|
||||
|
||||
Nach der Installation sollte eine vollständige Synchronisation durchgeführt werden:
|
||||
|
||||
```bash
|
||||
# Alle Buchungen synchronisieren
|
||||
./vendor/bin/sail artisan newsletter:sync-kulturreisen --force
|
||||
./vendor/bin/sail artisan newsletter:sync-ferienwohnungen --force
|
||||
```
|
||||
|
||||
## Verwendung
|
||||
|
||||
### Admin-Panel
|
||||
|
||||
Das Newsletter-Modul ist im Admin-Panel unter **CMS > Inhalte > Newsletter** erreichbar.
|
||||
|
||||
#### Hauptfunktionen:
|
||||
|
||||
1. **Kontaktliste**
|
||||
- Übersicht aller Newsletter-Kontakte
|
||||
- Filter nach Gruppe, Status, Herkunft
|
||||
- Suchfunktion nach E-Mail und Name
|
||||
- DataTables mit Sortierung und Pagination
|
||||
|
||||
2. **Kontakt-Detail**
|
||||
- Vollständige Kontaktinformationen
|
||||
- Buchungsstatistiken
|
||||
- Aktivitäts-Log
|
||||
- Verlinkung zu Original-Kundendaten
|
||||
|
||||
3. **Kontakt bearbeiten**
|
||||
- Manuelles Erstellen von Kontakten
|
||||
- Bearbeiten von Kontaktdaten
|
||||
- Status-Änderung
|
||||
- Gruppen-Zuweisung
|
||||
|
||||
4. **Synchronisation**
|
||||
- Manuelle Synchronisation über UI
|
||||
- Incremental Sync (letzte 30 Tage)
|
||||
- Full Sync (alle Buchungen)
|
||||
|
||||
5. **Export**
|
||||
- Export nach Gruppe
|
||||
- Export nach Status
|
||||
- CSV-Format
|
||||
- Vorkonfigurierte Export-Optionen
|
||||
|
||||
### Artisan-Befehle
|
||||
|
||||
#### Synchronisation Kulturreisen
|
||||
|
||||
```bash
|
||||
# Incremental Sync (letzte 30 Tage)
|
||||
./vendor/bin/sail artisan newsletter:sync-kulturreisen
|
||||
|
||||
# Full Sync (alle Buchungen)
|
||||
./vendor/bin/sail artisan newsletter:sync-kulturreisen --force
|
||||
```
|
||||
|
||||
Dieser Befehl:
|
||||
- Liest alle Buchungen aus der `booking` Tabelle
|
||||
- Verknüpft mit `customer` Daten
|
||||
- Erstellt oder aktualisiert Newsletter-Kontakte
|
||||
- Zählt Buchungen pro Kunde
|
||||
- Trackt letzte Buchung
|
||||
|
||||
#### Synchronisation Ferienwohnungen
|
||||
|
||||
```bash
|
||||
# Incremental Sync (letzte 30 Tage)
|
||||
./vendor/bin/sail artisan newsletter:sync-ferienwohnungen
|
||||
|
||||
# Full Sync (alle Buchungen)
|
||||
./vendor/bin/sail artisan newsletter:sync-ferienwohnungen --force
|
||||
```
|
||||
|
||||
Dieser Befehl:
|
||||
- Liest Buchungen aus `travel_user_booking_fewos` mit `invoice_number`
|
||||
- Nur Buchungen mit reiner Nummer (keine Storno etc.)
|
||||
- Verknüpft mit `travel_users` Daten
|
||||
- Erstellt oder aktualisiert Newsletter-Kontakte
|
||||
- Zählt Buchungen pro Kunde
|
||||
|
||||
### Automatische Synchronisation
|
||||
|
||||
Für regelmäßige Synchronisation empfiehlt sich ein Cron-Job:
|
||||
|
||||
```bash
|
||||
# In der crontab
|
||||
# Täglich um 2 Uhr morgens
|
||||
0 2 * * * cd /var/www/html && ./vendor/bin/sail artisan newsletter:sync-kulturreisen
|
||||
0 2 * * * cd /var/www/html && ./vendor/bin/sail artisan newsletter:sync-ferienwohnungen
|
||||
```
|
||||
|
||||
Oder im Laravel Scheduler (`app/Console/Kernel.php`):
|
||||
|
||||
```php
|
||||
protected function schedule(Schedule $schedule)
|
||||
{
|
||||
// Täglich um 2 Uhr
|
||||
$schedule->command('newsletter:sync-kulturreisen')->dailyAt('02:00');
|
||||
$schedule->command('newsletter:sync-ferienwohnungen')->dailyAt('02:15');
|
||||
}
|
||||
```
|
||||
|
||||
## Datenmodell
|
||||
|
||||
### Newsletter Contact
|
||||
|
||||
| Feld | Typ | Beschreibung |
|
||||
|------|-----|--------------|
|
||||
| `id` | bigint | Primary Key |
|
||||
| `email` | string | E-Mail-Adresse (unique) |
|
||||
| `firstname` | string | Vorname |
|
||||
| `lastname` | string | Nachname |
|
||||
| `group_kulturreisen` | boolean | Zugehörigkeit zur Gruppe Kulturreisen |
|
||||
| `group_ferienwohnungen` | boolean | Zugehörigkeit zur Gruppe Ferienwohnungen |
|
||||
| `source` | enum | Herkunft des Kontakts |
|
||||
| `status` | enum | Status (active, inactive, unsubscribed, bounced) |
|
||||
| `subscribed_at` | timestamp | Zeitpunkt der Anmeldung |
|
||||
| `unsubscribed_at` | timestamp | Zeitpunkt der Abmeldung |
|
||||
| `last_booking_at` | timestamp | Zeitpunkt der letzten Buchung |
|
||||
| `total_bookings_kulturreisen` | int | Anzahl Buchungen Kulturreisen |
|
||||
| `total_bookings_ferienwohnungen` | int | Anzahl Buchungen Ferienwohnungen |
|
||||
| `customer_id` | int | Referenz zu `customer` (Kulturreisen) |
|
||||
| `travel_user_id` | int | Referenz zu `travel_users` (Ferienwohnungen) |
|
||||
| `last_synced_at` | timestamp | Letzte Synchronisation |
|
||||
| `sync_hash` | string | Hash für Duplikat-Erkennung |
|
||||
| `notes` | text | Notizen |
|
||||
|
||||
### Newsletter Log
|
||||
|
||||
| Feld | Typ | Beschreibung |
|
||||
|------|-----|--------------|
|
||||
| `id` | bigint | Primary Key |
|
||||
| `newsletter_contact_id` | bigint | Foreign Key zu newsletter_contacts |
|
||||
| `action` | enum | Aktion (subscribed, unsubscribed, etc.) |
|
||||
| `description` | string | Beschreibung der Aktion |
|
||||
| `metadata` | json | Zusätzliche Daten |
|
||||
| `user_id` | int | Admin-User der die Aktion ausgeführt hat |
|
||||
|
||||
## Status-Typen
|
||||
|
||||
- **active**: Aktiver Newsletter-Empfänger
|
||||
- **inactive**: Inaktiv (z.B. temporär deaktiviert)
|
||||
- **unsubscribed**: Abgemeldet vom Newsletter
|
||||
- **bounced**: E-Mail nicht zustellbar (Bounced)
|
||||
|
||||
## Herkunfts-Typen
|
||||
|
||||
- **booking_kulturreisen**: Aus Kulturreisen-Buchung
|
||||
- **booking_ferienwohnungen**: Aus Ferienwohnungs-Buchung
|
||||
- **newsletter_signup**: Newsletter-Anmeldung über Formular
|
||||
- **manual**: Manuell erstellt
|
||||
- **import**: Über Import hinzugefügt
|
||||
|
||||
## Export-Formate
|
||||
|
||||
### CSV-Export
|
||||
|
||||
Der CSV-Export enthält folgende Spalten:
|
||||
|
||||
1. ID
|
||||
2. E-Mail
|
||||
3. Vorname
|
||||
4. Nachname
|
||||
5. Gruppe Kulturreisen (Ja/Nein)
|
||||
6. Gruppe Ferienwohnungen (Ja/Nein)
|
||||
7. Status
|
||||
8. Herkunft
|
||||
9. Buchungen Kulturreisen
|
||||
10. Buchungen Ferienwohnungen
|
||||
11. Letzte Buchung
|
||||
12. Angemeldet am
|
||||
13. Abgemeldet am
|
||||
14. Erstellt am
|
||||
|
||||
## Best Practices
|
||||
|
||||
### Duplikat-Vermeidung
|
||||
|
||||
Das System verwendet einen Hash basierend auf E-Mail und Quelle zur Duplikat-Erkennung. Kontakte werden automatisch zusammengeführt, wenn:
|
||||
- Dieselbe E-Mail-Adresse in beiden Systemen vorkommt
|
||||
- Der neuere Datensatz wird bevorzugt
|
||||
- Gruppenzugehörigkeiten werden kombiniert
|
||||
|
||||
### Datenschutz
|
||||
|
||||
- Soft-Delete: Gelöschte Kontakte werden nicht permanent entfernt
|
||||
- Log-Tracking: Alle Änderungen werden protokolliert
|
||||
- Abmelde-Funktion: Kontakte können sich jederzeit abmelden
|
||||
- Export-Kontrolle: Nur autorisierte Benutzer können exportieren
|
||||
|
||||
### Performance
|
||||
|
||||
- Incremental Sync: Standardmäßig werden nur die letzten 30 Tage synchronisiert
|
||||
- Indexierung: E-Mail, Status und Gruppen sind indexiert
|
||||
- DataTables: Server-Side Processing für große Datenmengen
|
||||
- Batch-Processing: Synchronisation verarbeitet Datensätze effizient
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Problem: Synchronisation schlägt fehl
|
||||
|
||||
**Lösung:**
|
||||
1. Prüfe Datenbankverbindungen (beide Datenbanken müssen erreichbar sein)
|
||||
2. Prüfe Logs: `storage/logs/laravel.log`
|
||||
3. Führe Sync mit `--force` aus für vollständige Synchronisation
|
||||
|
||||
### Problem: Duplikate in der Liste
|
||||
|
||||
**Lösung:**
|
||||
1. Prüfe `sync_hash` Spalte
|
||||
2. Führe manuelle Bereinigung durch
|
||||
3. Kontakte können manuell zusammengeführt werden
|
||||
|
||||
### Problem: Export enthält keine Daten
|
||||
|
||||
**Lösung:**
|
||||
1. Prüfe Filter-Einstellungen
|
||||
2. Prüfe Status der Kontakte
|
||||
3. Prüfe Berechtigungen
|
||||
|
||||
## API-Endpunkte
|
||||
|
||||
Alle Routen sind unter dem Middleware-Schutz `['admin', '2fa', 'auth.permission:cms-newsletter']`.
|
||||
|
||||
| Methode | Route | Beschreibung |
|
||||
|---------|-------|--------------|
|
||||
| GET | `/newsletter` | Liste aller Kontakte |
|
||||
| GET | `/newsletter/datatable` | DataTables AJAX |
|
||||
| GET | `/newsletter/{id}` | Detail eines Kontakts |
|
||||
| GET | `/newsletter/{id}/edit` | Bearbeitungsformular |
|
||||
| POST | `/newsletter/{id}/store` | Speichern |
|
||||
| DELETE | `/newsletter/{id}` | Löschen (soft delete) |
|
||||
| POST | `/newsletter/{id}/unsubscribe` | Abmelden |
|
||||
| POST | `/newsletter/{id}/resubscribe` | Wieder aktivieren |
|
||||
| POST | `/newsletter/sync` | Synchronisation starten |
|
||||
| GET | `/newsletter/export` | Export |
|
||||
|
||||
## Erweiterungen
|
||||
|
||||
### Integration mit Newsletter-Diensten
|
||||
|
||||
Das Modul kann einfach mit externen Newsletter-Diensten (Mailchimp, SendGrid, etc.) integriert werden:
|
||||
|
||||
```php
|
||||
// Beispiel: Export für Mailchimp
|
||||
$contacts = NewsletterContact::active()->kulturreisen()->get();
|
||||
foreach ($contacts as $contact) {
|
||||
// Mailchimp API Call
|
||||
}
|
||||
```
|
||||
|
||||
### Webhook für Abmeldungen
|
||||
|
||||
Eine öffentliche Abmelde-Route kann hinzugefügt werden:
|
||||
|
||||
```php
|
||||
// routes/web.php
|
||||
Route::get('/newsletter/unsubscribe/{token}', 'NewsletterPublicController@unsubscribe');
|
||||
```
|
||||
|
||||
### Double-Opt-In
|
||||
|
||||
Für Newsletter-Anmeldungen kann ein Double-Opt-In Prozess implementiert werden.
|
||||
|
||||
## Support
|
||||
|
||||
Bei Fragen oder Problemen:
|
||||
1. Prüfe diese Dokumentation
|
||||
2. Prüfe Laravel Logs
|
||||
3. Prüfe Artisan Command Output
|
||||
4. Kontaktiere das Entwickler-Team
|
||||
|
||||
## Changelog
|
||||
|
||||
### Version 1.0.0 (2025-11-07)
|
||||
- ✅ Initiales Release
|
||||
- ✅ Kontaktverwaltung
|
||||
- ✅ Synchronisation Kulturreisen
|
||||
- ✅ Synchronisation Ferienwohnungen
|
||||
- ✅ Export-Funktion
|
||||
- ✅ Admin-Panel Integration
|
||||
|
||||
151
init.sh
Normal file
|
|
@ -0,0 +1,151 @@
|
|||
#!/bin/bash
|
||||
|
||||
# Sterntours Laravel Projekt - Initialisierungsskript
|
||||
# Dieses Script führt alle notwendigen Schritte zur Initialisierung des Projekts aus
|
||||
|
||||
set -e # Beendet das Script bei Fehlern
|
||||
|
||||
echo "================================================"
|
||||
echo "Sterntours Laravel Projekt - Initialisierung"
|
||||
echo "================================================"
|
||||
echo ""
|
||||
|
||||
# Farben für Ausgabe
|
||||
GREEN='\033[0;32m'
|
||||
YELLOW='\033[1;33m'
|
||||
RED='\033[0;31m'
|
||||
NC='\033[0m' # No Color
|
||||
|
||||
# Funktion für Erfolgsmeldungen
|
||||
success() {
|
||||
echo -e "${GREEN}✓ $1${NC}"
|
||||
}
|
||||
|
||||
# Funktion für Info-Meldungen
|
||||
info() {
|
||||
echo -e "${YELLOW}➜ $1${NC}"
|
||||
}
|
||||
|
||||
# Funktion für Fehler
|
||||
error() {
|
||||
echo -e "${RED}✗ $1${NC}"
|
||||
}
|
||||
|
||||
# 1. Prüfe ob Docker läuft
|
||||
info "Prüfe Docker Installation..."
|
||||
if ! command -v docker &> /dev/null; then
|
||||
error "Docker ist nicht installiert!"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if ! docker info &> /dev/null; then
|
||||
error "Docker läuft nicht! Bitte starte Docker."
|
||||
exit 1
|
||||
fi
|
||||
success "Docker ist bereit"
|
||||
|
||||
# 2. Prüfe ob Composer installiert ist
|
||||
info "Prüfe Composer Installation..."
|
||||
if ! command -v composer &> /dev/null; then
|
||||
error "Composer ist nicht installiert!"
|
||||
exit 1
|
||||
fi
|
||||
success "Composer ist installiert"
|
||||
|
||||
# 3. .env Datei erstellen falls nicht vorhanden
|
||||
info "Prüfe .env Datei..."
|
||||
if [ ! -f .env ]; then
|
||||
if [ -f .env.example ]; then
|
||||
cp .env.example .env
|
||||
success ".env Datei erstellt"
|
||||
else
|
||||
error ".env.example nicht gefunden!"
|
||||
exit 1
|
||||
fi
|
||||
else
|
||||
success ".env Datei existiert bereits"
|
||||
fi
|
||||
|
||||
# 4. Composer Dependencies installieren
|
||||
info "Installiere Composer Dependencies..."
|
||||
composer install --no-interaction --prefer-dist --optimize-autoloader
|
||||
success "Composer Dependencies installiert"
|
||||
|
||||
# 5. NPM Dependencies installieren
|
||||
info "Prüfe Node.js Installation..."
|
||||
if command -v npm &> /dev/null; then
|
||||
info "Installiere NPM Dependencies..."
|
||||
npm install
|
||||
success "NPM Dependencies installiert"
|
||||
else
|
||||
error "Node.js/NPM nicht gefunden - überspringe NPM Installation"
|
||||
fi
|
||||
|
||||
# 6. Application Key generieren
|
||||
info "Generiere Application Key..."
|
||||
php artisan key:generate --force
|
||||
success "Application Key generiert"
|
||||
|
||||
# 7. Erstelle proxy Netzwerk falls nicht vorhanden
|
||||
info "Erstelle Docker Proxy Netzwerk..."
|
||||
if ! docker network inspect proxy &> /dev/null; then
|
||||
docker network create proxy
|
||||
success "Proxy Netzwerk erstellt"
|
||||
else
|
||||
success "Proxy Netzwerk existiert bereits"
|
||||
fi
|
||||
|
||||
# 8. Docker Container starten
|
||||
info "Starte Docker Container (Laravel Sail)..."
|
||||
./vendor/bin/sail up -d
|
||||
success "Docker Container gestartet"
|
||||
|
||||
# 9. Warte bis Datenbank bereit ist
|
||||
info "Warte auf Datenbank..."
|
||||
sleep 10
|
||||
success "Datenbank sollte bereit sein"
|
||||
|
||||
# 10. Datenbank Migrationen ausführen
|
||||
info "Führe Datenbank Migrationen aus..."
|
||||
./vendor/bin/sail artisan migrate --force
|
||||
success "Datenbank Migrationen ausgeführt"
|
||||
|
||||
# 11. Storage Links erstellen
|
||||
info "Erstelle Storage Links..."
|
||||
./vendor/bin/sail artisan storage:link
|
||||
success "Storage Links erstellt"
|
||||
|
||||
# 12. Cache leeren
|
||||
info "Leere Application Cache..."
|
||||
./vendor/bin/sail artisan config:clear
|
||||
./vendor/bin/sail artisan cache:clear
|
||||
./vendor/bin/sail artisan view:clear
|
||||
./vendor/bin/sail artisan route:clear
|
||||
success "Cache geleert"
|
||||
|
||||
# 13. Optimierung (optional)
|
||||
info "Optimiere Application..."
|
||||
./vendor/bin/sail artisan config:cache
|
||||
./vendor/bin/sail artisan route:cache
|
||||
./vendor/bin/sail artisan view:cache
|
||||
success "Application optimiert"
|
||||
|
||||
echo ""
|
||||
echo "================================================"
|
||||
echo -e "${GREEN}✓ Initialisierung erfolgreich abgeschlossen!${NC}"
|
||||
echo "================================================"
|
||||
echo ""
|
||||
echo "Dein Projekt ist nun bereit:"
|
||||
echo " - Haupt-URL: https://mein.sterntours.test"
|
||||
echo " - Alternative URL: https://sterntours.test"
|
||||
echo " - Mailpit Dashboard: https://sterntours-mail.test"
|
||||
echo " - Vite Assets: https://assets.sterntours.test"
|
||||
echo ""
|
||||
echo "Nützliche Befehle:"
|
||||
echo " - Container stoppen: ./vendor/bin/sail down"
|
||||
echo " - Container starten: ./vendor/bin/sail up -d"
|
||||
echo " - Logs anzeigen: ./vendor/bin/sail logs -f"
|
||||
echo " - Artisan Befehle: ./vendor/bin/sail artisan <command>"
|
||||
echo " - Tests ausführen: ./vendor/bin/sail test"
|
||||
echo ""
|
||||
|
||||
|
|
@ -1,4 +1,5 @@
|
|||
{
|
||||
"name": "STERN TOURS [DEV CONTAINER]",
|
||||
"folders": [
|
||||
{
|
||||
"path": "."
|
||||
|
|
|
|||
BIN
newsletter/assets/jordanien-1.jpg
Normal file
|
After Width: | Height: | Size: 108 KiB |
BIN
newsletter/assets/jordanien-1.png
Normal file
|
After Width: | Height: | Size: 1.7 MiB |
BIN
newsletter/assets/jordanien-2.jpg
Normal file
|
After Width: | Height: | Size: 102 KiB |
BIN
newsletter/assets/jordanien-2.png
Normal file
|
After Width: | Height: | Size: 1.8 MiB |
BIN
newsletter/assets/sterntours-logo.png
Normal file
|
After Width: | Height: | Size: 23 KiB |
BIN
newsletter/assets/usedom-1.jpg
Normal file
|
After Width: | Height: | Size: 130 KiB |
BIN
newsletter/assets/usedom-1.png
Normal file
|
After Width: | Height: | Size: 1.9 MiB |
BIN
newsletter/assets/usedom-2.jpg
Normal file
|
After Width: | Height: | Size: 100 KiB |
BIN
newsletter/assets/usedom-2.png
Normal file
|
After Width: | Height: | Size: 1.6 MiB |
BIN
newsletter/assets/wlogo.png
Normal file
|
After Width: | Height: | Size: 23 KiB |
376
newsletter/sterntours-nl1.html
Normal file
|
|
@ -0,0 +1,376 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="de">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<meta http-equiv="X-UA-Compatible" content="ie=edge">
|
||||
<title>STERN TOURS Newsletter</title>
|
||||
<!--[if mso]>
|
||||
<style type="text/css">
|
||||
table { border-collapse: collapse; border-spacing: 0; border: 0; }
|
||||
div, td { padding: 0; }
|
||||
div { margin: 0 !important; }
|
||||
</style>
|
||||
<noscript>
|
||||
<xml>
|
||||
<o:OfficeDocumentSettings>
|
||||
<o:PixelsPerInch>96</o:PixelsPerInch>
|
||||
</o:OfficeDocumentSettings>
|
||||
</xml>
|
||||
</noscript>
|
||||
<![endif]-->
|
||||
<style type="text/css">
|
||||
body {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
background-color: #f4f4f4;
|
||||
font-family: Arial, sans-serif;
|
||||
-webkit-text-size-adjust: 100%;
|
||||
-ms-text-size-adjust: 100%;
|
||||
}
|
||||
table { border-spacing: 0; border-collapse: collapse; }
|
||||
td { padding: 0; }
|
||||
img { border: 0; display: block; }
|
||||
|
||||
.wrapper {
|
||||
width: 100%;
|
||||
table-layout: fixed;
|
||||
background-color: #f4f4f4;
|
||||
padding: 40px 0 60px 0;
|
||||
}
|
||||
|
||||
.main {
|
||||
background-color: #ffffff;
|
||||
margin: 0 auto;
|
||||
width: 100%;
|
||||
max-width: 600px;
|
||||
border-spacing: 0;
|
||||
border-collapse: collapse;
|
||||
font-family: Arial, sans-serif;
|
||||
color: #333333;
|
||||
}
|
||||
|
||||
.button {
|
||||
background-color: #648859;
|
||||
color: #ffffff !important;
|
||||
text-decoration: none;
|
||||
padding: 15px 32px;
|
||||
font-weight: bold;
|
||||
font-size: 16px;
|
||||
display: inline-block;
|
||||
border-radius: 30px;
|
||||
box-shadow: 0 4px 15px rgba(0, 0, 0, 0.1);
|
||||
|
||||
}
|
||||
.img-round {
|
||||
border-radius: 10px;
|
||||
box-shadow: 0 4px 15px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.highlight-box {
|
||||
background-color: #648859;
|
||||
padding: 25px 30px;
|
||||
color: #ffffff;
|
||||
border-radius: 10px;
|
||||
}
|
||||
|
||||
.accent-bar {
|
||||
height: 4px;
|
||||
background-color: #648859;
|
||||
border: none;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.tip-box {
|
||||
background-color: #fef9f0;
|
||||
border-left: 4px solid #cb9030;
|
||||
padding: 20px 25px;
|
||||
}
|
||||
|
||||
/* Preheader verstecken - verbesserte Methode */
|
||||
.preheader {
|
||||
display: none !important;
|
||||
visibility: hidden;
|
||||
mso-hide: all;
|
||||
font-size: 1px;
|
||||
color: #f4f4f4;
|
||||
line-height: 1px;
|
||||
max-height: 0px;
|
||||
max-width: 0px;
|
||||
opacity: 0;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
h2 {
|
||||
color: #648859;
|
||||
font-size: 26px;
|
||||
line-height: 32px;
|
||||
font-weight: 700;
|
||||
margin: 0;
|
||||
font-family: Arial, sans-serif;
|
||||
}
|
||||
|
||||
h3 {
|
||||
color: #648859;
|
||||
font-size: 20px;
|
||||
line-height: 26px;
|
||||
font-weight: 600;
|
||||
margin: 0 0 12px 0;
|
||||
font-family: Arial, sans-serif;
|
||||
}
|
||||
|
||||
p {
|
||||
margin: 0;
|
||||
font-family: Arial, sans-serif;
|
||||
}
|
||||
|
||||
a {
|
||||
color: #648859;
|
||||
}
|
||||
|
||||
@media screen and (max-width: 600px) {
|
||||
.main {
|
||||
width: 100% !important;
|
||||
}
|
||||
.button {
|
||||
padding: 14px 28px !important;
|
||||
font-size: 15px !important;
|
||||
}
|
||||
h2 {
|
||||
font-size: 22px !important;
|
||||
line-height: 28px !important;
|
||||
}
|
||||
.mobile-padding {
|
||||
padding-left: 20px !important;
|
||||
padding-right: 20px !important;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body style="margin: 0; padding: 0; background-color: #f4f4f4; font-family: Arial, sans-serif;">
|
||||
<!-- Preheader -->
|
||||
<div class="preheader" style="display: none; visibility: hidden; mso-hide: all; font-size: 1px; color: #f4f4f4; line-height: 1px; max-height: 0px; max-width: 0px; opacity: 0; overflow: hidden;">
|
||||
Als Dankeschön für Ihre Treue haben wir einen exklusiven Vorteil für Sie...
|
||||
<!-- Leerraum, damit Preheader nicht zu kurz ist -->
|
||||
͏‌ ͏‌ ͏‌ ͏‌ ͏‌ ͏‌ ͏‌ ͏‌ ͏‌ ͏‌ ͏‌ ͏‌ ͏‌ ͏‌ ͏‌
|
||||
</div>
|
||||
|
||||
<!-- Wrapper -->
|
||||
<table role="presentation" class="wrapper" width="100%" cellpadding="0" cellspacing="0" border="0" style="width: 100%; background-color: #f4f4f4; padding: 40px 0 60px 0;">
|
||||
<tr>
|
||||
<td align="center" style="padding: 0;">
|
||||
|
||||
<!-- Main Container -->
|
||||
<table role="presentation" class="main" width="600" cellpadding="0" cellspacing="0" border="0" style="width: 600px; max-width: 600px; background-color: #ffffff; margin: 0 auto;">
|
||||
|
||||
<!-- Accent Bar -->
|
||||
<tr>
|
||||
<td class="accent-bar" style="height: 4px; background-color: #648859; padding: 0; line-height: 4px; font-size: 4px;">
|
||||
<!--[if gte mso 9]>
|
||||
<v:rect xmlns:v="urn:schemas-microsoft-com:vml" fill="true" stroke="false" style="width:600px;height:4px;">
|
||||
<v:fill type="gradient" color="#648859" color2="#cb9030" angle="90" />
|
||||
</v:rect>
|
||||
<![endif]-->
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<!-- Logo -->
|
||||
<tr>
|
||||
<td style="padding: 50px 0 15px 0; text-align: center; background-color: #ffffff;" align="center">
|
||||
<a href="https://www.sterntours.de" target="_blank" style="text-decoration: none; display: inline-block;">
|
||||
<img src="https://news.sterntours.de/assets/sterntours-logo.png" alt="STERN TOURS Logo" width="250" style="max-width: 250px; height: auto; margin: 0 auto; display: block; border: 0;">
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<!-- Tagline -->
|
||||
<tr>
|
||||
<td style="padding: 0 0 30px; text-align: center; background-color: #ffffff;" align="center">
|
||||
<p style="margin: 0; font-size: 15px; color: #648859; font-weight: 600; letter-spacing: 1px; text-transform: uppercase; font-family: Arial, sans-serif;">Ihr Kulturreise-Spezialist</p>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<!-- Intro Text -->
|
||||
<tr>
|
||||
<td class="mobile-padding" style="padding: 30px 40px;">
|
||||
<p style="margin: 0; font-size: 20px; font-weight: 600; line-height: 28px; color: #648859; font-family: Arial, sans-serif;">Liebe Reisefreundinnen und Reisefreunde,</p>
|
||||
<p style="margin: 20px 0 0; font-size: 16px; line-height: 26px; color: #444444; font-family: Arial, sans-serif;">
|
||||
vielleicht ist es schon eine Weile her, dass Sie mit uns die Welt entdeckt haben, und wir hoffen, Sie erinnern sich gerne an Ihre Reise mit STERN TOURS zurück.
|
||||
</p>
|
||||
<p style="margin: 16px 0 0; font-size: 16px; line-height: 26px; color: #444444; font-family: Arial, sans-serif;">
|
||||
Nach einer längeren Pause möchten wir in Zukunft wieder regelmäßiger mit Ihnen in Kontakt treten – nicht mit aufdringlicher Werbung, sondern mit echten Inspirationen, exklusiven Einblicken und besonderen Angeboten, die wir speziell für unsere treuen Kunden zusammenstellen.
|
||||
</p>
|
||||
<p style="margin: 16px 0 0; font-size: 16px; line-height: 26px; color: #444444; font-family: Arial, sans-serif;">
|
||||
Als Auftakt möchten wir Sie an einen Ort entführen, der gerade in der kalten Jahreszeit seine ganze Magie entfaltet.
|
||||
</p>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<!-- Main Content - Jordanien -->
|
||||
<tr>
|
||||
<td class="mobile-padding" style="padding: 20px 40px 30px 40px;">
|
||||
<h2 style="margin: 0 0 20px 0; font-size: 26px; color: #648859; line-height: 32px; font-weight: 700; font-family: Arial, sans-serif;">Entfliehen Sie dem Grau:<br>Jordaniens Wintersonne ruft!</h2>
|
||||
<a href="https://www.sterntours.de/jordanien-reisen" target="_blank" style="text-decoration: none; display: block;">
|
||||
<img class="img-round" src="https://news.sterntours.de/assets/jordanien-1.jpg" alt="Felsenstadt Petra in Jordanien" width="520" style="width: 100%; max-width: 520px; height: auto; margin: 0 auto; display: block; border: 0;">
|
||||
</a>
|
||||
<p style="margin: 24px 0 0; font-size: 16px; line-height: 26px; color: #444444; font-family: Arial, sans-serif;">
|
||||
Während es bei uns ungemütlich wird, erwarten Sie in Jordanien <strong style="color: #648859; font-weight: bold;">angenehme Tagestemperaturen</strong>, weniger Besucher an den weltberühmten Stätten und ein unvergleichliches Licht in der Wüste. Entdecken Sie das Felsenwunder Petra in aller Ruhe, erleben Sie die Weite des Wadi Rum bei einer Jeep-Tour und lassen Sie sich im Toten Meer treiben.
|
||||
<br><br>
|
||||
Unsere erfahrenen Reiseleiter zeigen Ihnen die Schätze des Haschemitischen Königreichs.
|
||||
</p>
|
||||
|
||||
<!-- Button mit VML für Outlook -->
|
||||
<table role="presentation" width="100%" cellpadding="0" cellspacing="0" border="0" style="margin: 30px 0 0;">
|
||||
<tr>
|
||||
<td align="center" style="padding: 0;">
|
||||
<!--[if mso]>
|
||||
<v:roundrect xmlns:v="urn:schemas-microsoft-com:vml" xmlns:w="urn:schemas-microsoft-com:office:word" href="https://www.sterntours.de/jordanien-reisen" style="height:50px;v-text-anchor:middle;width:350px;" arcsize="60%" strokecolor="#648859" fillcolor="#648859">
|
||||
<w:anchorlock/>
|
||||
<center style="color:#ffffff;font-family:Arial,sans-serif;font-size:16px;font-weight:bold;">Jetzt unsere Jordanien-Reisen entdecken</center>
|
||||
</v:roundrect>
|
||||
<![endif]-->
|
||||
<!--[if !mso]><!-->
|
||||
<a href="https://www.sterntours.de/jordanien-reisen" target="_blank" class="button" style="background-color: #648859; color: #ffffff; text-decoration: none; padding: 15px 32px; font-weight: bold; font-size: 16px; display: inline-block; font-family: Arial, sans-serif; mso-hide: all;">Jetzt unsere Jordanien-Reisen entdecken</a>
|
||||
<!--<![endif]-->
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<!-- Loyalty Offer Box -->
|
||||
<tr>
|
||||
<td class="mobile-padding" style="padding: 20px 40px;">
|
||||
<table role="presentation" width="100%" cellpadding="0" cellspacing="0" border="0" style="width: 100%;">
|
||||
<tr>
|
||||
<td class="highlight-box" style="background-color: #648859; padding: 30px; font-family: Arial, sans-serif;">
|
||||
<h3 style="margin: 0 0 12px 0; font-size: 22px; color: #ffffff; font-weight: 600; font-family: Arial, sans-serif;">Ein Dankeschön für Ihre Treue:<br>100 € Rabatt auf Ihre nächste Buchung</h3>
|
||||
<p style="margin: 0; font-size: 16px; line-height: 25px; color: #ffffff; font-family: Arial, sans-serif;">
|
||||
Als kleines Willkommen zurück und als Dankeschön für Ihr Vertrauen möchten wir Ihnen den Start ins nächste Abenteuer erleichtern.
|
||||
<br><br>
|
||||
Wir schenken Ihnen <strong style="background-color: #577a4d; padding: 2px 6px; font-weight: bold;">100 € Rabatt</strong> auf Ihre nächste Buchung einer unserer Kulturreisen (Angebot gültig für alle neuen Buchungen bis zum 31.12.2025).
|
||||
</p>
|
||||
<p style="margin: 15px 0 0; font-size: 14px; line-height: 22px; color: #f0f0f0; font-style: italic; font-family: Arial, sans-serif;">
|
||||
Geben Sie bei Ihrer Buchung einfach den Code <strong style="font-weight: bold;">ENTDECKER100</strong> im Anmerkungen-Feld mit an. Der Rabatt wird dann automatisch bei Ihrer Buchungsbestätigung berücksichtigt und vom Gesamtpreis abgezogen.
|
||||
</p>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<!-- Tip Box -->
|
||||
<tr>
|
||||
<td class="mobile-padding" style="padding: 30px 40px;">
|
||||
<table role="presentation" width="100%" cellpadding="0" cellspacing="0" border="0" style="width: 100%;">
|
||||
<tr>
|
||||
<td class="tip-box" style="background-color: #fef9f0; border-left: 4px solid #cb9030; padding: 25px 30px; font-family: Arial, sans-serif;">
|
||||
<h3 style="margin: 0 0 12px 0; font-size: 19px; color: #cb9030; font-weight: 600; font-family: Arial, sans-serif;">Wussten Sie schon? Unser Tipp aus Amman</h3>
|
||||
<p style="margin: 0; font-size: 16px; line-height: 25px; color: #444444; font-family: Arial, sans-serif;">
|
||||
Kein Besuch in Jordaniens Hauptstadt ist komplett, ohne <strong style="color: #cb9030; font-weight: bold;">Knafeh</strong> probiert zu haben! Diese süße, warme Käsespeise mit Sirup ist eine lokale Delikatesse und der perfekte Abschluss eines erlebnisreichen Tages. Fragen Sie unsere Reiseleiter nach dem besten Laden dafür!
|
||||
</p>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<!-- Closing -->
|
||||
<tr>
|
||||
<td class="mobile-padding" style="padding: 30px 40px;">
|
||||
<p style="margin: 0; font-size: 16px; line-height: 26px; color: #444444; font-family: Arial, sans-serif;">
|
||||
Wir freuen uns darauf, bald wieder von Ihnen zu hören und vielleicht schon Ihre nächste Traumreise zu planen.
|
||||
</p>
|
||||
<p style="margin: 25px 0 0; font-size: 16px; line-height: 26px; color: #444444; font-family: Arial, sans-serif;">
|
||||
Herzliche Grüße aus Berlin,<br><br>
|
||||
<strong style="color: #648859; font-size: 17px; font-weight: bold;">Leoni Stern</strong><br>
|
||||
<span style="color: #666666;">und das gesamte Team von STERN TOURS</span>
|
||||
</p>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<!-- Cross-Reference Box - Usedom -->
|
||||
<tr>
|
||||
<td class="mobile-padding" style="padding: 30px 40px 20px 40px;">
|
||||
<table role="presentation" width="100%" cellpadding="0" cellspacing="0" border="0" style="width: 100%;">
|
||||
<tr>
|
||||
<td style="background-color: #fffbee; border: 2px solid #ffc926; padding: 30px; font-family: Arial, sans-serif;">
|
||||
<h3 style="margin: 0 0 12px 0; font-size: 20px; color: #ffc926; font-weight: 600; font-family: Arial, sans-serif;">Kennen Sie schon unsere Sonnenseite an der Ostsee?</h3>
|
||||
<p style="margin: 0; font-size: 16px; line-height: 25px; color: #444444; font-family: Arial, sans-serif;">
|
||||
Manchmal muss es gar nicht die weite Ferne sein. Für eine erholsame Auszeit zwischendurch bieten wir exklusive Ferienwohnungen auf der Sonneninsel Usedom an – perfekt für ein langes Wochenende oder die Familienferien.
|
||||
</p>
|
||||
|
||||
<!-- Usedom Button -->
|
||||
<table role="presentation" width="100%" cellpadding="0" cellspacing="0" border="0" style="margin: 25px 0 0;">
|
||||
<tr>
|
||||
<td align="center" style="padding: 0;">
|
||||
<!--[if mso]>
|
||||
<v:roundrect xmlns:v="urn:schemas-microsoft-com:vml" xmlns:w="urn:schemas-microsoft-com:office:word" href="https://www.sterntours.de/ferienwohnungen" style="height:48px;v-text-anchor:middle;width:380px;" arcsize="60%" strokecolor="#cb9030" fillcolor="#cb9030">
|
||||
<w:anchorlock/>
|
||||
<center style="color:#ffffff;font-family:Arial,sans-serif;font-size:15px;font-weight:bold;">Unsere Ferienwohnungen auf Usedom entdecken</center>
|
||||
</v:roundrect>
|
||||
<![endif]-->
|
||||
<!--[if !mso]><!-->
|
||||
<a href="https://www.sterntours.de/ferienwohnungen" target="_blank" class="button" style="background-color: #cb9030; color: #ffffff; text-decoration: none; padding: 14px 28px; font-weight: bold; font-size: 15px; display: inline-block; font-family: Arial, sans-serif; mso-hide: all;">Unsere Ferienwohnungen auf Usedom entdecken</a>
|
||||
<!--<![endif]-->
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<!-- Divider -->
|
||||
<tr>
|
||||
<td style="padding: 20px 40px;">
|
||||
<table role="presentation" width="100%" cellpadding="0" cellspacing="0" border="0" style="width: 100%;">
|
||||
<tr>
|
||||
<td style="padding: 0; border-bottom: 1px solid #e0e0e0;"></td>
|
||||
</tr>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<!-- Newsletter Info -->
|
||||
<tr>
|
||||
<td class="mobile-padding" style="padding: 30px 40px;">
|
||||
<p style="margin: 0; font-size: 13px; line-height: 20px; color: #444444; font-family: Arial, sans-serif;">
|
||||
<b style="font-weight: bold;">Warum erhalten Sie diesen Newsletter?</b> Sie erhalten diese E-Mail, da Sie in der Vergangenheit eine Reise bei STERN TOURS gebucht haben.
|
||||
Wir möchten Sie als geschätzten Kunden auch zukünftig über relevante Angebote und Inspirationen informieren. Sie können dem widersprechen oder sich
|
||||
jederzeit <a href="https://newsletter.sterntours.de/fl/5b6f50c5-c043-4a53-b207-541de55a2a69/?pid=[USER_ID_SECURE]&mid=[MAILING_ID]&gid=[CTID]" target="_blank" style="color: #648859; text-decoration: underline;">abmelden</a>.
|
||||
</p>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<!-- Footer -->
|
||||
<tr>
|
||||
<td class="mobile-padding" style="padding: 30px 40px 40px 40px; text-align: center; background-color: #f9f9f9;" align="center">
|
||||
<p style="margin: 0; font-size: 13px; color: #666666; line-height: 20px; font-family: Arial, sans-serif;">
|
||||
<strong style="color: #648859; font-weight: bold;">STERN TOURS GmbH</strong><br>
|
||||
Emser Straße 3 | 10719 Berlin<br>
|
||||
Telefon: <a href="tel:+493070094100" style="color: #648859; text-decoration: none;">030 700 94 100</a> |
|
||||
E-Mail: <a href="mailto:stern@sterntours.de" style="color: #648859; text-decoration: none;">stern@sterntours.de</a>
|
||||
</p>
|
||||
<p style="margin: 20px 0 0; font-size: 12px; font-family: Arial, sans-serif;">
|
||||
<a href="https://www.sterntours.de/impressum/" target="_blank" style="color: #888888; text-decoration: none; padding: 0 10px;">Impressum</a> |
|
||||
<a href="https://www.sterntours.de/datenschutz/" target="_blank" style="color: #888888; text-decoration: none; padding: 0 10px;">Datenschutz</a> |
|
||||
<a href="https://newsletter.sterntours.de/fl/5b6f50c5-c043-4a53-b207-541de55a2a69/?pid=[USER_ID_SECURE]&mid=[MAILING_ID]&gid=[CTID]" target="_blank" style="color: #888888; text-decoration: none; padding: 0 10px;">Newsletter abbestellen</a>
|
||||
</p>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
</table>
|
||||
<!-- Ende Main Container -->
|
||||
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
<!-- Ende Wrapper -->
|
||||
|
||||
<!-- Tracking Pixel -->
|
||||
<img src="https://stats-eu2.crsend.com/stats/mc_[CLIENT_ID]_[MAILING_ID]_[USER_ID_SECURE].gif" border="0" alt="" height="1" width="1" style="display: block; border: 0; height: 1px; width: 1px;">
|
||||
<img src="https://newsletter.sterntours.de/op2/[CLIENT_ID]-[MAILING_ID]/[USER_AES]/logo.gif" border="0" alt="" height="1" width="1" style="display: block; border: 0; height: 1px; width: 1px;">
|
||||
</body>
|
||||
</html>
|
||||
378
newsletter/usedom-nl1.html
Normal file
|
|
@ -0,0 +1,378 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="de">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<meta http-equiv="X-UA-Compatible" content="ie=edge">
|
||||
<title>STERN TOURS Newsletter - Usedom</title>
|
||||
<!--[if mso]>
|
||||
<style type="text/css">
|
||||
table { border-collapse: collapse; border-spacing: 0; border: 0; }
|
||||
div, td { padding: 0; }
|
||||
div { margin: 0 !important; }
|
||||
</style>
|
||||
<noscript>
|
||||
<xml>
|
||||
<o:OfficeDocumentSettings>
|
||||
<o:PixelsPerInch>96</o:PixelsPerInch>
|
||||
</o:OfficeDocumentSettings>
|
||||
</xml>
|
||||
</noscript>
|
||||
<![endif]-->
|
||||
<style type="text/css">
|
||||
body {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
background-color: #f4f4f4;
|
||||
font-family: Arial, sans-serif;
|
||||
-webkit-text-size-adjust: 100%;
|
||||
-ms-text-size-adjust: 100%;
|
||||
}
|
||||
table { border-spacing: 0; border-collapse: collapse; }
|
||||
td { padding: 0; }
|
||||
img { border: 0; display: block; }
|
||||
|
||||
.wrapper {
|
||||
width: 100%;
|
||||
table-layout: fixed;
|
||||
background-color: #f4f4f4;
|
||||
padding: 40px 0 60px 0;
|
||||
}
|
||||
|
||||
.main {
|
||||
background-color: #ffffff;
|
||||
margin: 0 auto;
|
||||
width: 100%;
|
||||
max-width: 600px;
|
||||
border-spacing: 0;
|
||||
border-collapse: collapse;
|
||||
font-family: Arial, sans-serif;
|
||||
color: #333333;
|
||||
}
|
||||
|
||||
.button {
|
||||
background-color: #648859;
|
||||
color: #ffffff !important;
|
||||
text-decoration: none;
|
||||
padding: 15px 32px;
|
||||
font-weight: bold;
|
||||
font-size: 16px;
|
||||
display: inline-block;
|
||||
border-radius: 30px;
|
||||
box-shadow: 0 4px 15px rgba(0, 0, 0, 0.1);
|
||||
|
||||
|
||||
}
|
||||
|
||||
.img-round {
|
||||
border-radius: 10px;
|
||||
box-shadow: 0 4px 15px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.highlight-box {
|
||||
background-color: #648859;
|
||||
padding: 25px 30px;
|
||||
color: #ffffff;
|
||||
border-radius: 10px;
|
||||
}
|
||||
|
||||
.accent-bar {
|
||||
height: 4px;
|
||||
background-color: #648859;
|
||||
border: none;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.tip-box {
|
||||
background-color: #fef9f0;
|
||||
border-left: 4px solid #cb9030;
|
||||
padding: 20px 25px;
|
||||
}
|
||||
|
||||
/* Preheader verstecken - verbesserte Methode */
|
||||
.preheader {
|
||||
display: none !important;
|
||||
visibility: hidden;
|
||||
mso-hide: all;
|
||||
font-size: 1px;
|
||||
color: #f4f4f4;
|
||||
line-height: 1px;
|
||||
max-height: 0px;
|
||||
max-width: 0px;
|
||||
opacity: 0;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
h2 {
|
||||
color: #648859;
|
||||
font-size: 26px;
|
||||
line-height: 32px;
|
||||
font-weight: 700;
|
||||
margin: 0;
|
||||
font-family: Arial, sans-serif;
|
||||
}
|
||||
|
||||
h3 {
|
||||
color: #648859;
|
||||
font-size: 20px;
|
||||
line-height: 26px;
|
||||
font-weight: 600;
|
||||
margin: 0 0 12px 0;
|
||||
font-family: Arial, sans-serif;
|
||||
}
|
||||
|
||||
p {
|
||||
margin: 0;
|
||||
font-family: Arial, sans-serif;
|
||||
}
|
||||
|
||||
a {
|
||||
color: #648859;
|
||||
}
|
||||
|
||||
@media screen and (max-width: 600px) {
|
||||
.main {
|
||||
width: 100% !important;
|
||||
}
|
||||
.button {
|
||||
padding: 14px 28px !important;
|
||||
font-size: 15px !important;
|
||||
}
|
||||
h2 {
|
||||
font-size: 22px !important;
|
||||
line-height: 28px !important;
|
||||
}
|
||||
.mobile-padding {
|
||||
padding-left: 20px !important;
|
||||
padding-right: 20px !important;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body style="margin: 0; padding: 0; background-color: #f4f4f4; font-family: Arial, sans-serif;">
|
||||
<!-- Preheader -->
|
||||
<div class="preheader" style="display: none; visibility: hidden; mso-hide: all; font-size: 1px; color: #f4f4f4; line-height: 1px; max-height: 0px; max-width: 0px; opacity: 0; overflow: hidden;">
|
||||
Genießen Sie die ruhige Jahreszeit mit einem kleinen Dankeschön für Ihre Treue...
|
||||
<!-- Leerraum, damit Preheader nicht zu kurz ist -->
|
||||
͏‌ ͏‌ ͏‌ ͏‌ ͏‌ ͏‌ ͏‌ ͏‌ ͏‌ ͏‌ ͏‌ ͏‌ ͏‌ ͏‌ ͏‌
|
||||
</div>
|
||||
|
||||
<!-- Wrapper -->
|
||||
<table role="presentation" class="wrapper" width="100%" cellpadding="0" cellspacing="0" border="0" style="width: 100%; background-color: #f4f4f4; padding: 40px 0 60px 0;">
|
||||
<tr>
|
||||
<td align="center" style="padding: 0;">
|
||||
|
||||
<!-- Main Container -->
|
||||
<table role="presentation" class="main" width="600" cellpadding="0" cellspacing="0" border="0" style="width: 600px; max-width: 600px; background-color: #ffffff; margin: 0 auto;">
|
||||
|
||||
<!-- Accent Bar -->
|
||||
<tr>
|
||||
<td class="accent-bar" style="height: 4px; background-color: #648859; padding: 0; line-height: 4px; font-size: 4px;">
|
||||
<!--[if gte mso 9]>
|
||||
<v:rect xmlns:v="urn:schemas-microsoft-com:vml" fill="true" stroke="false" style="width:600px;height:4px;">
|
||||
<v:fill type="gradient" color="#648859" color2="#cb9030" angle="90" />
|
||||
</v:rect>
|
||||
<![endif]-->
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<!-- Logo -->
|
||||
<tr>
|
||||
<td style="padding: 50px 0 15px 0; text-align: center; background-color: #ffffff;" align="center">
|
||||
<a href="https://www.sterntours.de" target="_blank" style="text-decoration: none; display: inline-block;">
|
||||
<img src="https://news.sterntours.de/assets/sterntours-logo.png" alt="STERN TOURS Logo" width="250" style="max-width: 250px; height: auto; margin: 0 auto; display: block; border: 0;">
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<!-- Tagline -->
|
||||
<tr>
|
||||
<td style="padding: 0 0 30px; text-align: center; background-color: #ffffff;" align="center">
|
||||
<p style="margin: 0; font-size: 15px; color: #648859; font-weight: 600; letter-spacing: 1px; text-transform: uppercase; font-family: Arial, sans-serif;">Ihre Auszeit auf Usedom</p>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<!-- Intro Text -->
|
||||
<tr>
|
||||
<td class="mobile-padding" style="padding: 30px 40px;">
|
||||
<p style="margin: 0; font-size: 20px; font-weight: 600; line-height: 28px; color: #648859; font-family: Arial, sans-serif;">Liebe Usedom-Freunde,</p>
|
||||
<p style="margin: 20px 0 0; font-size: 16px; line-height: 26px; color: #444444; font-family: Arial, sans-serif;">
|
||||
vielleicht ist es schon eine Weile her, dass Sie bei uns auf der Sonneninsel zu Gast waren, und wir hoffen, Sie hatten eine wunderbar erholsame Zeit in unseren Ferienwohnungen.
|
||||
</p>
|
||||
<p style="margin: 16px 0 0; font-size: 16px; line-height: 26px; color: #444444; font-family: Arial, sans-serif;">
|
||||
Nach einer längeren Pause möchten wir in Zukunft wieder regelmäßiger mit Ihnen in Kontakt treten – mit saisonalen Insel-Tipps, Informationen zu freien Terminen und exklusiven Angeboten, die wir speziell für unsere treuen Gäste zusammenstellen.
|
||||
</p>
|
||||
<p style="margin: 16px 0 0; font-size: 16px; line-height: 26px; color: #444444; font-family: Arial, sans-serif;">
|
||||
Als Auftakt möchten wir Ihnen die Jahreszeit schmackhaft machen, die für viele Kenner die schönste auf der Insel ist.
|
||||
</p>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<!-- Main Content - Usedom -->
|
||||
<tr>
|
||||
<td class="mobile-padding" style="padding: 20px 40px 30px 40px;">
|
||||
<h2 style="margin: 0 0 20px 0; font-size: 26px; color: #648859; line-height: 32px; font-weight: 700; font-family: Arial, sans-serif;">Die Magie der stillen Jahreszeit:<br>Ihr Rückzugsort auf Usedom</h2>
|
||||
<a href="https://www.sterntours.de/ferienwohnungen" target="_blank" style="text-decoration: none; display: block;">
|
||||
<img class="img-round" src="https://news.sterntours.de/assets/usedom-2.jpg" alt="Leere Seebrücke auf Usedom im Herbst" width="520" style="width: 100%; max-width: 520px; height: auto; margin: 0 auto; display: block; border: 0;">
|
||||
</a>
|
||||
<p style="margin: 24px 0 0; font-size: 16px; line-height: 26px; color: #444444; font-family: Arial, sans-serif;">
|
||||
Wenn die großen Touristenströme verschwunden sind, zeigt Usedom sein wahres Gesicht. Genießen Sie <strong style="color: #648859; font-weight: bold;">endlose Strandspaziergänge</strong> in klarer, kalter Luft, wärmen Sie sich danach in der Sauna oder vor dem Kamin auf und erleben Sie die einzigartige Ruhe, die nur der Herbst und Winter an der See bieten können.
|
||||
<br><br>
|
||||
Unsere gemütlichen Ferienwohnungen sind der perfekte Ausgangspunkt für Ihre persönliche Auszeit.
|
||||
</p>
|
||||
|
||||
<!-- Button mit VML für Outlook -->
|
||||
<table role="presentation" width="100%" cellpadding="0" cellspacing="0" border="0" style="margin: 30px 0 0;">
|
||||
<tr>
|
||||
<td align="center" style="padding: 0;">
|
||||
<!--[if mso]>
|
||||
<v:roundrect xmlns:v="urn:schemas-microsoft-com:vml" xmlns:w="urn:schemas-microsoft-com:office:word" href="https://www.sterntours.de/ferienwohnungen" style="height:50px;v-text-anchor:middle;width:350px;" arcsize="60%" strokecolor="#648859" fillcolor="#648859">
|
||||
<w:anchorlock/>
|
||||
<center style="color:#ffffff;font-family:Arial,sans-serif;font-size:16px;font-weight:bold;">Freie Termine und Wohnungen anzeigen</center>
|
||||
</v:roundrect>
|
||||
<![endif]-->
|
||||
<!--[if !mso]><!-->
|
||||
<a href="https://www.sterntours.de/ferienwohnungen" target="_blank" class="button" style="background-color: #648859; color: #ffffff; text-decoration: none; padding: 15px 32px; font-weight: bold; font-size: 16px; display: inline-block; font-family: Arial, sans-serif; mso-hide: all;">Freie Termine und Wohnungen anzeigen</a>
|
||||
<!--<![endif]-->
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<!-- Loyalty Offer Box -->
|
||||
<tr>
|
||||
<td class="mobile-padding" style="padding: 20px 40px;">
|
||||
<table role="presentation" width="100%" cellpadding="0" cellspacing="0" border="0" style="width: 100%;">
|
||||
<tr>
|
||||
<td class="highlight-box" style="background-color: #648859; padding: 30px; font-family: Arial, sans-serif;">
|
||||
<h3 style="margin: 0 0 12px 0; font-size: 22px; color: #ffffff; font-weight: 600; font-family: Arial, sans-serif;">Ein Dankeschön für Ihre Treue:<br>10% Rabatt auf Ihre nächste Buchung</h3>
|
||||
<p style="margin: 0; font-size: 16px; line-height: 25px; color: #ffffff; font-family: Arial, sans-serif;">
|
||||
Als kleines Willkommen zurück und als Dankeschön für Ihr Vertrauen möchten wir Ihnen etwas zurückgeben. Für Ihre nächste Buchung in einer unserer Ferienwohnungen auf Usedom schenken wir Ihnen <strong style="background-color: #577a4d; padding: 2px 6px; font-weight: bold;">10% Rabatt auf den reinen Buchungspreis.</strong>
|
||||
<br><br>
|
||||
Vielleicht die perfekte Gelegenheit für eine spontane Herbst-Auszeit oder um schon jetzt die ersten sonnigen Frühlingstage im nächsten Jahr zu planen?
|
||||
</p>
|
||||
<p style="margin: 15px 0 0; font-size: 14px; line-height: 22px; color: #f0f0f0; font-style: italic; font-family: Arial, sans-serif;">
|
||||
Geben Sie bei Ihrer Anfrage oder Buchung einfach das Stichwort <strong style="font-weight: bold;">INSEL-TREUE10</strong> im Anmerkungen-Feld an, der Rabatt wird Ihnen in der Bestätigung automatisch abgezogen. (Angebot gültig für alle neuen Buchungen bis zum 31.12.2025)
|
||||
</p>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<!-- Tip Box -->
|
||||
<tr>
|
||||
<td class="mobile-padding" style="padding: 30px 40px;">
|
||||
<table role="presentation" width="100%" cellpadding="0" cellspacing="0" border="0" style="width: 100%;">
|
||||
<tr>
|
||||
<td class="tip-box" style="background-color: #fef9f0; border-left: 4px solid #cb9030; padding: 25px 30px; font-family: Arial, sans-serif;">
|
||||
<h3 style="margin: 0 0 12px 0; font-size: 19px; color: #cb9030; font-weight: 600; font-family: Arial, sans-serif;">Unser Insel-Tipp für gemütliche Tage</h3>
|
||||
<p style="margin: 0; font-size: 16px; line-height: 25px; color: #444444; font-family: Arial, sans-serif;">
|
||||
Wenn das Wetter mal richtig ungemütlich ist, gibt es nichts Besseres als einen Besuch in der <strong style="color: #cb9030; font-weight: bold;">Bernsteintherme in Zinnowitz</strong>. Lassen Sie sich im warmen Meerwasser-Becken treiben oder genießen Sie einen entspannten Saunagang. Ein perfekter Plan B für einen grauen Tag!
|
||||
</p>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<!-- Closing -->
|
||||
<tr>
|
||||
<td class="mobile-padding" style="padding: 30px 40px;">
|
||||
<p style="margin: 0; font-size: 16px; line-height: 26px; color: #444444; font-family: Arial, sans-serif;">
|
||||
Wir freuen uns darauf, Sie bald wieder als unsere Gäste auf der wunderschönen Insel Usedom begrüßen zu dürfen.
|
||||
</p>
|
||||
<p style="margin: 25px 0 0; font-size: 16px; line-height: 26px; color: #444444; font-family: Arial, sans-serif;">
|
||||
Herzliche Grüße aus Berlin,<br><br>
|
||||
<strong style="color: #648859; font-size: 17px; font-weight: bold;">Leoni Stern</strong><br>
|
||||
<span style="color: #666666;">und das gesamte Team von STERN TOURS</span>
|
||||
</p>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<!-- Cross-Reference Box - Kulturreisen -->
|
||||
<tr>
|
||||
<td class="mobile-padding" style="padding: 30px 40px 20px 40px;">
|
||||
<table role="presentation" width="100%" cellpadding="0" cellspacing="0" border="0" style="width: 100%;">
|
||||
<tr>
|
||||
<td style="background-color: #fffbee; border: 2px solid #ffc926; padding: 30px; font-family: Arial, sans-serif;">
|
||||
<h3 style="margin: 0 0 12px 0; font-size: 20px; color: #ffc926; font-weight: 600; font-family: Arial, sans-serif;">Packt Sie manchmal das Fernweh?</h3>
|
||||
<p style="margin: 0; font-size: 16px; line-height: 25px; color: #444444; font-family: Arial, sans-serif;">
|
||||
Neben unseren Wohlfühl-Oasen auf Usedom sind wir seit über 25 Jahren Spezialisten für unvergessliche Kulturreisen. Falls Sie mal wieder Lust auf 1001 Nacht, antike Wunder oder orientalische Märkte haben, schauen Sie sich unsere geführten Reisen an.
|
||||
</p>
|
||||
|
||||
<!-- Kulturreisen Button -->
|
||||
<table role="presentation" width="100%" cellpadding="0" cellspacing="0" border="0" style="margin: 25px 0 0;">
|
||||
<tr>
|
||||
<td align="center" style="padding: 0;">
|
||||
<!--[if mso]>
|
||||
<v:roundrect xmlns:v="urn:schemas-microsoft-com:vml" xmlns:w="urn:schemas-microsoft-com:office:word" href="https://www.sterntours.de/jordanien-reisen" style="height:48px;v-text-anchor:middle;width:380px;" arcsize="60%" strokecolor="#cb9030" fillcolor="#cb9030">
|
||||
<w:anchorlock/>
|
||||
<center style="color:#ffffff;font-family:Arial,sans-serif;font-size:15px;font-weight:bold;">Unsere Kulturreisen in den Orient entdecken</center>
|
||||
</v:roundrect>
|
||||
<![endif]-->
|
||||
<!--[if !mso]><!-->
|
||||
<a href="https://www.sterntours.de/jordanien-reisen" target="_blank" class="button" style="background-color: #cb9030; color: #ffffff; text-decoration: none; padding: 14px 28px; font-weight: bold; font-size: 15px; display: inline-block; font-family: Arial, sans-serif; mso-hide: all;">Unsere Kulturreisen in den Orient entdecken</a>
|
||||
<!--<![endif]-->
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<!-- Divider -->
|
||||
<tr>
|
||||
<td style="padding: 20px 40px;">
|
||||
<table role="presentation" width="100%" cellpadding="0" cellspacing="0" border="0" style="width: 100%;">
|
||||
<tr>
|
||||
<td style="padding: 0; border-bottom: 1px solid #e0e0e0;"></td>
|
||||
</tr>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<!-- Newsletter Info -->
|
||||
<tr>
|
||||
<td class="mobile-padding" style="padding: 30px 40px;">
|
||||
<p style="margin: 0; font-size: 13px; line-height: 20px; color: #444444; font-family: Arial, sans-serif;">
|
||||
<b style="font-weight: bold;">Warum erhalten Sie diesen Newsletter?</b> Sie erhalten diese E-Mail, da Sie in der Vergangenheit eine Reise bei STERN TOURS gebucht haben.
|
||||
Wir möchten Sie als geschätzten Kunden auch zukünftig über relevante Angebote und Inspirationen informieren. Sie können dem widersprechen oder sich
|
||||
jederzeit <a href="https://newsletter.sterntours.de/fl/d0f5e3af-9a4f-406f-abfa-d6eb2843e21e/?pid=[USER_ID_SECURE]&mid=[MAILING_ID]&gid=[CTID]" target="_blank" style="color: #648859; text-decoration: underline;">abmelden</a>.
|
||||
</p>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<!-- Footer -->
|
||||
<tr>
|
||||
<td class="mobile-padding" style="padding: 30px 40px 40px 40px; text-align: center; background-color: #f9f9f9;" align="center">
|
||||
<p style="margin: 0; font-size: 13px; color: #666666; line-height: 20px; font-family: Arial, sans-serif;">
|
||||
<strong style="color: #648859; font-weight: bold;">STERN TOURS GmbH</strong><br>
|
||||
Emser Straße 3 | 10719 Berlin<br>
|
||||
Telefon: <a href="tel:+493070094100" style="color: #648859; text-decoration: none;">030 700 94 100</a> |
|
||||
E-Mail: <a href="mailto:stern@sterntours.de" style="color: #648859; text-decoration: none;">stern@sterntours.de</a>
|
||||
</p>
|
||||
<p style="margin: 20px 0 0; font-size: 12px; font-family: Arial, sans-serif;">
|
||||
<a href="https://www.sterntours.de/impressum/" target="_blank" style="color: #888888; text-decoration: none; padding: 0 10px;">Impressum</a> |
|
||||
<a href="https://www.sterntours.de/datenschutz/" target="_blank" style="color: #888888; text-decoration: none; padding: 0 10px;">Datenschutz</a> |
|
||||
<a href="https://newsletter.sterntours.de/fl/d0f5e3af-9a4f-406f-abfa-d6eb2843e21e/?pid=[USER_ID_SECURE]&mid=[MAILING_ID]&gid=[CTID]" target="_blank" style="color: #888888; text-decoration: none; padding: 0 10px;">Newsletter abbestellen</a>
|
||||
</p>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
</table>
|
||||
<!-- Ende Main Container -->
|
||||
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
<!-- Ende Wrapper -->
|
||||
|
||||
<!-- Tracking Pixel -->
|
||||
<img src="https://stats-eu2.crsend.com/stats/mc_[CLIENT_ID]_[MAILING_ID]_[USER_ID_SECURE].gif" border="0" alt="" height="1" width="1" style="display: block; border: 0; height: 1px; width: 1px;">
|
||||
<img src="https://newsletter.sterntours.de/op2/[CLIENT_ID]-[MAILING_ID]/[USER_AES]/logo.gif" border="0" alt="" height="1" width="1" style="display: block; border: 0; height: 1px; width: 1px;">
|
||||
</body>
|
||||
</html>
|
||||
|
|
@ -112,7 +112,7 @@ class LfmController extends Controller
|
|||
{
|
||||
$shared_folder_name = config('lfm.shared_folder_name');
|
||||
$shared_folder = IQContentFolder::where('name', $shared_folder_name)->where('folder_id', null)->first();
|
||||
if(!$shared_folder){
|
||||
if (!$shared_folder) {
|
||||
IQContentFolder::create([
|
||||
'folder_id' => null,
|
||||
'name' => $shared_folder_name,
|
||||
|
|
|
|||
|
|
@ -25,46 +25,98 @@ class UploadController extends LfmController
|
|||
*/
|
||||
public function upload()
|
||||
{
|
||||
$uploaded_files = request()->file('upload');
|
||||
$error_bag = [];
|
||||
$new_filename = null;
|
||||
try {
|
||||
// Prüfe, ob überhaupt Dateien hochgeladen wurden
|
||||
$uploaded_files = request()->file('upload');
|
||||
$error_bag = [];
|
||||
$new_filename = null;
|
||||
|
||||
foreach (is_array($uploaded_files) ? $uploaded_files : [$uploaded_files] as $file) {
|
||||
try {
|
||||
$new_filename = $this->lfm->upload($file);
|
||||
} catch (\Exception $e) {
|
||||
Log::error($e->getMessage(), [
|
||||
'file' => $e->getFile(),
|
||||
'line' => $e->getLine(),
|
||||
'trace' => $e->getTraceAsString()
|
||||
// Prüfe auf post_max_size Überschreitung
|
||||
if (empty($uploaded_files) && empty($_FILES) && $_SERVER['REQUEST_METHOD'] == 'POST') {
|
||||
$postMaxSize = ini_get('post_max_size');
|
||||
$uploadMaxSize = ini_get('upload_max_filesize');
|
||||
|
||||
$error_bag[] = "Die Datei ist zu groß für den Upload. " .
|
||||
"Maximale Uploadgröße: {$uploadMaxSize}. " .
|
||||
"Maximale POST-Größe: {$postMaxSize}. " .
|
||||
"Bitte kontaktieren Sie den Administrator, um die Limits zu erhöhen.";
|
||||
|
||||
Log::error('Upload fehlgeschlagen: post_max_size oder upload_max_filesize überschritten', [
|
||||
'post_max_size' => $postMaxSize,
|
||||
'upload_max_filesize' => $uploadMaxSize,
|
||||
'content_length' => $_SERVER['CONTENT_LENGTH'] ?? 'unknown'
|
||||
]);
|
||||
array_push($error_bag, $e->getMessage());
|
||||
}
|
||||
}
|
||||
} elseif (empty($uploaded_files)) {
|
||||
$error_bag[] = "Es wurde keine Datei zum Upload ausgewählt.";
|
||||
} else {
|
||||
foreach (is_array($uploaded_files) ? $uploaded_files : [$uploaded_files] as $file) {
|
||||
try {
|
||||
$new_filename = $this->lfm->upload($file);
|
||||
} catch (\Exception $e) {
|
||||
$errorMessage = $e->getMessage();
|
||||
|
||||
if (is_array($uploaded_files)) {
|
||||
$response = count($error_bag) > 0 ? $error_bag : parent::$success_response;
|
||||
} else { // upload via ckeditor5 expects json responses
|
||||
if (is_null($new_filename)) {
|
||||
$response = ['error' =>
|
||||
[
|
||||
'message' => $error_bag[0]
|
||||
// Sicherstellen, dass die Fehlermeldung ein String ist
|
||||
if (is_object($errorMessage) || is_array($errorMessage)) {
|
||||
$errorMessage = json_encode($errorMessage);
|
||||
}
|
||||
|
||||
Log::error('Upload-Fehler: ' . $errorMessage, [
|
||||
'file_name' => $file ? $file->getClientOriginalName() : 'unknown',
|
||||
'file_size' => $file ? $file->getSize() : 0,
|
||||
'error_file' => $e->getFile(),
|
||||
'error_line' => $e->getLine(),
|
||||
'trace' => $e->getTraceAsString()
|
||||
]);
|
||||
|
||||
array_push($error_bag, $errorMessage);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Fehlerbehandlung
|
||||
if (count($error_bag) > 0) {
|
||||
$errorMsg = isset($error_bag[0]) ? $error_bag[0] : 'Unbekannter Upload-Fehler';
|
||||
|
||||
// Einheitliches Fehlerformat für Frontend
|
||||
$response = [
|
||||
'error' => [
|
||||
'message' => $errorMsg
|
||||
]
|
||||
];
|
||||
} else {
|
||||
|
||||
/*$response = view(Lfm::PACKAGE_NAME . '::use')
|
||||
->withFile($this->lfm->setName($new_filename)->url());
|
||||
*/
|
||||
// HTTP 400 Status für Fehler
|
||||
return response()->json($response, 400);
|
||||
}
|
||||
|
||||
// Erfolgreiche Antwort
|
||||
if (is_array($uploaded_files)) {
|
||||
$response = parent::$success_response;
|
||||
} else { // upload via ckeditor5 expects json responses
|
||||
$url = $this->lfm->setName($new_filename)->url();
|
||||
|
||||
$response = [
|
||||
'url' => $url
|
||||
];
|
||||
}
|
||||
|
||||
return response()->json($response);
|
||||
} catch (\Exception $e) {
|
||||
// Fange alle unerwarteten Fehler ab
|
||||
Log::error('Unerwarteter Upload-Fehler: ' . $e->getMessage(), [
|
||||
'error_file' => $e->getFile(),
|
||||
'error_line' => $e->getLine(),
|
||||
'trace' => $e->getTraceAsString(),
|
||||
'request_data' => [
|
||||
'content_length' => $_SERVER['CONTENT_LENGTH'] ?? 'unknown',
|
||||
'post_max_size' => ini_get('post_max_size'),
|
||||
'upload_max_filesize' => ini_get('upload_max_filesize'),
|
||||
]
|
||||
]);
|
||||
|
||||
return response()->json([
|
||||
'error' => [
|
||||
'message' => 'Ein unerwarteter Fehler ist aufgetreten: ' . $e->getMessage()
|
||||
]
|
||||
], 500);
|
||||
}
|
||||
return response()->json($response);
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@ use phpDocumentor\Reflection\DocBlock\Tags\Return_;
|
|||
use Symfony\Component\HttpFoundation\File\UploadedFile;
|
||||
use IqContent\LaravelFilemanager\Events\ImageIsUploading;
|
||||
use IqContent\LaravelFilemanager\Events\ImageWasUploaded;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
|
||||
class LfmPath
|
||||
{
|
||||
|
|
@ -65,18 +66,18 @@ class LfmPath
|
|||
{
|
||||
$parent_folder_id = $this->getModelParentFolderId();
|
||||
if ($this->isDirectory()) {
|
||||
if($this->folder_model == null){
|
||||
if($parent_folder_id){
|
||||
if ($this->folder_model == null) {
|
||||
if ($parent_folder_id) {
|
||||
$this->folder_model = IQContentFolder::where('name', $this->item_name)->where('folder_id', $parent_folder_id)->first();
|
||||
} else{
|
||||
} else {
|
||||
$this->folder_model = new IQContentFolder();
|
||||
}
|
||||
}
|
||||
}else{
|
||||
if($this->file_model == null){
|
||||
if($parent_folder_id){
|
||||
} else {
|
||||
if ($this->file_model == null) {
|
||||
if ($parent_folder_id) {
|
||||
$this->file_model = IQContentFile::where('name', $this->item_name)->where('folder_id', $parent_folder_id)->first();
|
||||
}else{
|
||||
} else {
|
||||
$this->file_model = new IQContentFile();
|
||||
}
|
||||
}
|
||||
|
|
@ -94,27 +95,29 @@ class LfmPath
|
|||
{
|
||||
if ($this->isDirectory()) {
|
||||
return $this->folder_model;
|
||||
}else{
|
||||
} else {
|
||||
return $this->file_model;
|
||||
}
|
||||
}
|
||||
|
||||
public function getModelParentFolderId(){
|
||||
public function getModelParentFolderId()
|
||||
{
|
||||
$parent_folder = $this->getModelFolderByPath();
|
||||
if($parent_folder) {
|
||||
if ($parent_folder) {
|
||||
return $parent_folder->id;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
public function getModelFolderByPath($parent = false){
|
||||
public function getModelFolderByPath($parent = false)
|
||||
{
|
||||
$working_dir = $this->path('working_dir');
|
||||
$working_dir = substr($working_dir, 0, strrpos($working_dir, '/'));
|
||||
$dirs = explode( "/", $working_dir);
|
||||
$dirs = explode("/", $working_dir);
|
||||
$folder_id = null;
|
||||
$folder = null;
|
||||
foreach ($dirs as $dir){
|
||||
foreach ($dirs as $dir) {
|
||||
$folder = IQContentFolder::where('name', $dir)->where('folder_id', $folder_id)->first();
|
||||
if($folder){
|
||||
if ($folder) {
|
||||
$folder_id = $folder->id;
|
||||
$this->parent_dir = $folder;
|
||||
}
|
||||
|
|
@ -156,7 +159,7 @@ class LfmPath
|
|||
|
||||
public function url()
|
||||
{
|
||||
return $this->storage->url($this->path('url'));
|
||||
return $this->storage->url($this->path('url'));
|
||||
}
|
||||
|
||||
public function folders()
|
||||
|
|
@ -171,16 +174,16 @@ class LfmPath
|
|||
return $this->sortByColumn($folders);
|
||||
}
|
||||
|
||||
public function recrusiveFolders($parent){
|
||||
public function recrusiveFolders($parent)
|
||||
{
|
||||
$folders = $this->folders();
|
||||
$b = [];
|
||||
foreach ($folders as $folder){
|
||||
foreach ($folders as $folder) {
|
||||
$b[$folder->name()] = $folder;
|
||||
$lfm = $folder->getLfm();
|
||||
$lfm->dir($parent);
|
||||
$a = $lfm->recrusiveFolders($folder->url());
|
||||
$b[$folder->name()."-childs"] = $a;
|
||||
|
||||
$b[$folder->name() . "-childs"] = $a;
|
||||
}
|
||||
return $b;
|
||||
}
|
||||
|
|
@ -229,19 +232,18 @@ class LfmPath
|
|||
$parent_folder_id = $this->getModelParentFolderId();
|
||||
$this->storage->makeDirectory(0777, true, true);
|
||||
|
||||
if(!$this->is_thumb){
|
||||
if (!$this->is_thumb) {
|
||||
IQContentFolder::create([
|
||||
'folder_id' => $parent_folder_id,
|
||||
'name' => $this->item_name,
|
||||
'identifier' => $this->item_name,
|
||||
]);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
public function isDirectory()
|
||||
{
|
||||
if($this->isDirectory !== null){
|
||||
if ($this->isDirectory !== null) {
|
||||
return $this->isDirectory;
|
||||
}
|
||||
$working_dir = $this->path('working_dir');
|
||||
|
|
@ -313,7 +315,7 @@ class LfmPath
|
|||
public function upload($file)
|
||||
{
|
||||
$error = $this->uploadValidator($file);
|
||||
if($error !== 'pass'){
|
||||
if ($error !== 'pass') {
|
||||
return false;
|
||||
}
|
||||
$new_file_name = $this->getNewName($file);
|
||||
|
|
@ -325,8 +327,12 @@ class LfmPath
|
|||
try {
|
||||
$new_file_name = $this->saveFile($file, $new_file_name);
|
||||
} catch (\Exception $e) {
|
||||
\Log::info($e);
|
||||
return $this->error('invalid');
|
||||
Log::error('File upload failed: ' . $e->getMessage(), [
|
||||
'file' => $e->getFile(),
|
||||
'line' => $e->getLine(),
|
||||
'trace' => $e->getTraceAsString()
|
||||
]);
|
||||
throw new \Exception($this->helper->getError('invalid') . ' ' . $e->getMessage());
|
||||
}
|
||||
IQContentFile::create([
|
||||
'folder_id' => $working_folder_id,
|
||||
|
|
@ -339,7 +345,7 @@ class LfmPath
|
|||
'content' => '',
|
||||
]);
|
||||
// TODO should be "FileWasUploaded"
|
||||
// event(new ImageWasUploaded($new_file_path));
|
||||
// event(new ImageWasUploaded($new_file_path));
|
||||
return $new_file_name;
|
||||
}
|
||||
|
||||
|
|
@ -352,7 +358,21 @@ class LfmPath
|
|||
} elseif ($file->getError() == UPLOAD_ERR_INI_SIZE) {
|
||||
return $this->error('file-size', ['max' => ini_get('upload_max_filesize')]);
|
||||
} elseif ($file->getError() != UPLOAD_ERR_OK) {
|
||||
throw new \Exception('File failed to upload. Error code: ' . $file->getError());
|
||||
$errorCode = $file->getError();
|
||||
$errorMessages = [
|
||||
UPLOAD_ERR_INI_SIZE => 'upload-1',
|
||||
UPLOAD_ERR_FORM_SIZE => 'upload-2',
|
||||
UPLOAD_ERR_PARTIAL => 'upload-3',
|
||||
UPLOAD_ERR_NO_FILE => 'upload-4',
|
||||
UPLOAD_ERR_NO_TMP_DIR => 'upload-6',
|
||||
UPLOAD_ERR_CANT_WRITE => 'upload-7',
|
||||
UPLOAD_ERR_EXTENSION => 'upload-8',
|
||||
];
|
||||
|
||||
$errorKey = isset($errorMessages[$errorCode]) ? $errorMessages[$errorCode] : 'upload-unknown';
|
||||
$errorParams = $errorKey === 'upload-unknown' ? ['code' => $errorCode] : [];
|
||||
|
||||
throw new \Exception($this->helper->getError($errorKey, $errorParams));
|
||||
}
|
||||
|
||||
$new_file_name = $this->getNewName($file);
|
||||
|
|
@ -422,7 +442,7 @@ class LfmPath
|
|||
// generate cropped image content
|
||||
$this->setName($file_name)->thumb(true);
|
||||
$image = Image::make($original_image->get());
|
||||
$this->image_dimensions = $image->width()."x".$image->height();
|
||||
$this->image_dimensions = $image->width() . "x" . $image->height();
|
||||
$image->fit(config('lfm.thumb_img_width', 200), config('lfm.thumb_img_height', 200));
|
||||
|
||||
$this->storage->put($image->stream()->detach());
|
||||
|
|
@ -437,9 +457,8 @@ class LfmPath
|
|||
$this->setName($file_name)->thumb(true);
|
||||
|
||||
$image = Image::make(file_get_contents($url));
|
||||
// $this->image_dimensions = $image->width()."x".$image->height();
|
||||
// $this->image_dimensions = $image->width()."x".$image->height();
|
||||
$image->fit(config('lfm.thumb_img_width', 200), config('lfm.thumb_img_height', 200));
|
||||
$this->storage->put($image->stream()->detach());
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@ return [
|
|||
'nav-thumbnails' => 'Thumbnails',
|
||||
'nav-list' => 'List',
|
||||
'nav-sort' => 'Sort',
|
||||
'nav-sort-alphabetic'=> 'Sort By Alphabets',
|
||||
'nav-sort-alphabetic' => 'Sort By Alphabets',
|
||||
'nav-sort-time' => 'Sort By Time',
|
||||
|
||||
'menu-rename' => 'Umbenennen',
|
||||
|
|
@ -64,11 +64,19 @@ return [
|
|||
'error-cannotnewdirectory' => 'Sie sind nicht berechtigt, neue Ordner zu erstellen',
|
||||
'error-cannotrename' => 'Sie sind nicht berechtigt, Ordner / Dateien umzubenennen',
|
||||
'error-cannotresize' => 'Sie sind nicht berechtigt, die Dateigröße zu ändern',
|
||||
'error-folder-not-found'=> 'Folder not found! (:folder)',
|
||||
'error-folder-not-found' => 'Folder not found! (:folder)',
|
||||
'error-size' => 'Over limit size:',
|
||||
'error-move-exist' => 'Datei existiert bereits.',
|
||||
'error-move-same' => 'Datei und Ziel sind gleich.',
|
||||
'error-move-parent' => 'Unterordner kann nicht verschoben werden.',
|
||||
'error-upload-1' => 'Die Datei überschreitet die in der PHP-Konfiguration (upload_max_filesize) festgelegte maximale Größe.',
|
||||
'error-upload-2' => 'Die Datei überschreitet die im HTML-Formular (MAX_FILE_SIZE) festgelegte maximale Größe.',
|
||||
'error-upload-3' => 'Die Datei wurde nur teilweise hochgeladen.',
|
||||
'error-upload-4' => 'Es wurde keine Datei hochgeladen.',
|
||||
'error-upload-6' => 'Es fehlt ein temporärer Ordner auf dem Server.',
|
||||
'error-upload-7' => 'Fehler beim Schreiben der Datei auf die Festplatte.',
|
||||
'error-upload-8' => 'Eine PHP-Erweiterung hat den Upload gestoppt.',
|
||||
'error-upload-unknown' => 'Ein unbekannter Upload-Fehler ist aufgetreten (Error Code: :code).',
|
||||
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -1,290 +1,408 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=EDGE" />
|
||||
<meta name="viewport" content="width=device-width,initial-scale=1">
|
||||
<meta charset="UTF-8">
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=EDGE" />
|
||||
<meta name="viewport" content="width=device-width,initial-scale=1">
|
||||
|
||||
<!-- Chrome, Firefox OS and Opera -->
|
||||
<meta name="theme-color" content="#333844">
|
||||
<!-- Windows Phone -->
|
||||
<meta name="msapplication-navbutton-color" content="#333844">
|
||||
<!-- iOS Safari -->
|
||||
<meta name="apple-mobile-web-app-status-bar-style" content="#333844">
|
||||
<!-- Chrome, Firefox OS and Opera -->
|
||||
<meta name="theme-color" content="#333844">
|
||||
<!-- Windows Phone -->
|
||||
<meta name="msapplication-navbutton-color" content="#333844">
|
||||
<!-- iOS Safari -->
|
||||
<meta name="apple-mobile-web-app-status-bar-style" content="#333844">
|
||||
|
||||
<title>{{ trans('laravel-filemanager::lfm.title-page') }}</title>
|
||||
<link rel="shortcut icon" type="image/png" href="{{ asset('vendor/laravel-filemanager/img/72px color.png') }}">
|
||||
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/4.1.0/css/bootstrap.min.css">
|
||||
<link rel="stylesheet" href="https://use.fontawesome.com/releases/v5.5.0/css/all.css">
|
||||
<link rel="stylesheet" href="//cdnjs.cloudflare.com/ajax/libs/jqueryui/1.11.2/jquery-ui.min.css">
|
||||
<link rel="stylesheet" href="{{ asset('vendor/laravel-filemanager/css/cropper.min.css') }}">
|
||||
<link rel="stylesheet" href="{{ asset('vendor/laravel-filemanager/css/dropzone.min.css') }}">
|
||||
<link rel="stylesheet" href="{{ asset('vendor/laravel-filemanager/css/mime-icons.min.css') }}">
|
||||
<style>{!! \File::get(base_path('vendor/iqcontent/laravel-filemanager/public/css/lfm.css')) !!}</style>
|
||||
{{-- Use the line below instead of the above if you need to cache the css. --}}
|
||||
{{-- <link rel="stylesheet" href="{{ asset('/vendor/laravel-filemanager/css/lfm.css') }}"> --}}
|
||||
<title>{{ trans('laravel-filemanager::lfm.title-page') }}</title>
|
||||
<link rel="shortcut icon" type="image/png" href="{{ asset('vendor/laravel-filemanager/img/72px color.png') }}">
|
||||
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/4.1.0/css/bootstrap.min.css">
|
||||
<link rel="stylesheet" href="https://use.fontawesome.com/releases/v5.5.0/css/all.css">
|
||||
<link rel="stylesheet" href="//cdnjs.cloudflare.com/ajax/libs/jqueryui/1.11.2/jquery-ui.min.css">
|
||||
<link rel="stylesheet" href="{{ asset('vendor/laravel-filemanager/css/cropper.min.css') }}">
|
||||
<link rel="stylesheet" href="{{ asset('vendor/laravel-filemanager/css/dropzone.min.css') }}">
|
||||
<link rel="stylesheet" href="{{ asset('vendor/laravel-filemanager/css/mime-icons.min.css') }}">
|
||||
<style>
|
||||
{!! \File::get(base_path('vendor/iqcontent/laravel-filemanager/public/css/lfm.css')) !!}
|
||||
</style>
|
||||
{{-- Use the line below instead of the above if you need to cache the css. --}}
|
||||
{{-- <link rel="stylesheet" href="{{ asset('/vendor/laravel-filemanager/css/lfm.css') }}"> --}}
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<nav class="navbar sticky-top navbar-expand-lg navbar-dark" id="nav">
|
||||
<a class="navbar-brand invisible-lg d-none d-lg-inline" id="to-previous">
|
||||
<i class="fas fa-arrow-left fa-fw"></i>
|
||||
<span class="d-none d-lg-inline">{{ trans('laravel-filemanager::lfm.nav-back') }}</span>
|
||||
</a>
|
||||
<a class="navbar-brand d-block d-lg-none" id="show_tree">
|
||||
<i class="fas fa-bars fa-fw"></i>
|
||||
</a>
|
||||
<a class="navbar-brand d-block d-lg-none" id="current_folder"></a>
|
||||
<a id="loading" class="navbar-brand"><i class="fas fa-spinner fa-spin"></i></a>
|
||||
<div class="ml-auto px-2">
|
||||
<a class="navbar-link d-none" id="multi_selection_toggle">
|
||||
<i class="fa fa-check-double fa-fw"></i>
|
||||
<span class="d-none d-lg-inline">{{ trans('laravel-filemanager::lfm.menu-multiple') }}</span>
|
||||
</a>
|
||||
</div>
|
||||
<a class="navbar-toggler collapsed border-0 px-1 py-2 m-0" data-toggle="collapse" data-target="#nav-buttons">
|
||||
<i class="fas fa-cog fa-fw"></i>
|
||||
</a>
|
||||
<div class="collapse navbar-collapse flex-grow-0" id="nav-buttons">
|
||||
<ul class="navbar-nav">
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" data-display="grid">
|
||||
<i class="fas fa-th-large fa-fw"></i>
|
||||
<span>{{ trans('laravel-filemanager::lfm.nav-thumbnails') }}</span>
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" data-display="list">
|
||||
<i class="fas fa-list-ul fa-fw"></i>
|
||||
<span>{{ trans('laravel-filemanager::lfm.nav-list') }}</span>
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item dropdown">
|
||||
<a class="nav-link dropdown-toggle" data-toggle="dropdown" role="button" aria-haspopup="true" aria-expanded="false">
|
||||
<i class="fas fa-sort fa-fw"></i>{{ trans('laravel-filemanager::lfm.nav-sort') }}
|
||||
</a>
|
||||
<div class="dropdown-menu dropdown-menu-right border-0"></div>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
<nav class="bg-light fixed-bottom border-top d-none" id="actions">
|
||||
<a data-action="open" data-multiple="false"><i class="fas fa-folder-open"></i>{{ trans('laravel-filemanager::lfm.btn-open') }}</a>
|
||||
<a data-action="preview" data-multiple="true"><i class="fas fa-images"></i>{{ trans('laravel-filemanager::lfm.menu-view') }}</a>
|
||||
<a data-action="use" data-multiple="true"><i class="fas fa-check"></i>{{ trans('laravel-filemanager::lfm.btn-confirm') }}</a>
|
||||
</nav>
|
||||
|
||||
<div class="d-flex flex-row">
|
||||
<div id="tree"></div>
|
||||
|
||||
<div id="main">
|
||||
<div id="alerts"></div>
|
||||
|
||||
<nav aria-label="breadcrumb" class="d-none d-lg-block" id="breadcrumbs">
|
||||
<ol class="breadcrumb">
|
||||
<li class="breadcrumb-item invisible">Home</li>
|
||||
</ol>
|
||||
</nav>
|
||||
|
||||
<div id="empty" class="d-none">
|
||||
<i class="fa fa-folder-open"></i>
|
||||
{{ trans('laravel-filemanager::lfm.message-empty') }}
|
||||
</div>
|
||||
|
||||
<div id="content"></div>
|
||||
|
||||
<a id="item-template" class="d-none">
|
||||
<div class="square"></div>
|
||||
|
||||
<div class="info">
|
||||
<div class="item_name text-truncate"></div>
|
||||
<time class="text-muted font-weight-light text-truncate"></time>
|
||||
<nav class="navbar sticky-top navbar-expand-lg navbar-dark" id="nav">
|
||||
<a class="navbar-brand invisible-lg d-none d-lg-inline" id="to-previous">
|
||||
<i class="fas fa-arrow-left fa-fw"></i>
|
||||
<span class="d-none d-lg-inline">{{ trans('laravel-filemanager::lfm.nav-back') }}</span>
|
||||
</a>
|
||||
<a class="navbar-brand d-block d-lg-none" id="show_tree">
|
||||
<i class="fas fa-bars fa-fw"></i>
|
||||
</a>
|
||||
<a class="navbar-brand d-block d-lg-none" id="current_folder"></a>
|
||||
<a id="loading" class="navbar-brand"><i class="fas fa-spinner fa-spin"></i></a>
|
||||
<div class="ml-auto px-2">
|
||||
<a class="navbar-link d-none" id="multi_selection_toggle">
|
||||
<i class="fa fa-check-double fa-fw"></i>
|
||||
<span class="d-none d-lg-inline">{{ trans('laravel-filemanager::lfm.menu-multiple') }}</span>
|
||||
</a>
|
||||
</div>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div id="fab"></div>
|
||||
</div>
|
||||
|
||||
<div class="modal fade" id="uploadModal" tabindex="-1" role="dialog" aria-labelledby="myModalLabel" aria-hidden="true">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h4 class="modal-title" id="myModalLabel">{{ trans('laravel-filemanager::lfm.title-upload') }}</h4>
|
||||
<button type="button" class="close" data-dismiss="modal" aria-label="Close"><span aia-hidden="true">×</span></button>
|
||||
<a class="navbar-toggler collapsed border-0 px-1 py-2 m-0" data-toggle="collapse" data-target="#nav-buttons">
|
||||
<i class="fas fa-cog fa-fw"></i>
|
||||
</a>
|
||||
<div class="collapse navbar-collapse flex-grow-0" id="nav-buttons">
|
||||
<ul class="navbar-nav">
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" data-display="grid">
|
||||
<i class="fas fa-th-large fa-fw"></i>
|
||||
<span>{{ trans('laravel-filemanager::lfm.nav-thumbnails') }}</span>
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" data-display="list">
|
||||
<i class="fas fa-list-ul fa-fw"></i>
|
||||
<span>{{ trans('laravel-filemanager::lfm.nav-list') }}</span>
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item dropdown">
|
||||
<a class="nav-link dropdown-toggle" data-toggle="dropdown" role="button" aria-haspopup="true"
|
||||
aria-expanded="false">
|
||||
<i class="fas fa-sort fa-fw"></i>{{ trans('laravel-filemanager::lfm.nav-sort') }}
|
||||
</a>
|
||||
<div class="dropdown-menu dropdown-menu-right border-0"></div>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<form action="{{ route('iqcontent.lfm.upload') }}" role='form' id='uploadForm' name='uploadForm' method='post' enctype='multipart/form-data' class="dropzone">
|
||||
<div class="form-group" id="attachment">
|
||||
<div class="controls text-center">
|
||||
<div class="input-group w-100">
|
||||
<a class="btn btn-primary w-100 text-white" id="upload-button">{{ trans('laravel-filemanager::lfm.message-choose') }}</a>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
<nav class="bg-light fixed-bottom border-top d-none" id="actions">
|
||||
<a data-action="open" data-multiple="false"><i
|
||||
class="fas fa-folder-open"></i>{{ trans('laravel-filemanager::lfm.btn-open') }}</a>
|
||||
<a data-action="preview" data-multiple="true"><i
|
||||
class="fas fa-images"></i>{{ trans('laravel-filemanager::lfm.menu-view') }}</a>
|
||||
<a data-action="use" data-multiple="true"><i
|
||||
class="fas fa-check"></i>{{ trans('laravel-filemanager::lfm.btn-confirm') }}</a>
|
||||
</nav>
|
||||
|
||||
<div class="d-flex flex-row">
|
||||
<div id="tree"></div>
|
||||
|
||||
<div id="main">
|
||||
<div id="alerts"></div>
|
||||
|
||||
<nav aria-label="breadcrumb" class="d-none d-lg-block" id="breadcrumbs">
|
||||
<ol class="breadcrumb">
|
||||
<li class="breadcrumb-item invisible">Home</li>
|
||||
</ol>
|
||||
</nav>
|
||||
|
||||
<div id="empty" class="d-none">
|
||||
<i class="fa fa-folder-open"></i>
|
||||
{{ trans('laravel-filemanager::lfm.message-empty') }}
|
||||
</div>
|
||||
<input type='hidden' name='working_dir' id='working_dir'>
|
||||
<input type='hidden' name='type' id='type' value='{{ request("type") }}'>
|
||||
<input type='hidden' name='_token' value='{{csrf_token()}}'>
|
||||
</form>
|
||||
|
||||
<div id="content"></div>
|
||||
|
||||
<a id="item-template" class="d-none">
|
||||
<div class="square"></div>
|
||||
|
||||
<div class="info">
|
||||
<div class="item_name text-truncate"></div>
|
||||
<time class="text-muted font-weight-light text-truncate"></time>
|
||||
</div>
|
||||
</a>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-secondary w-100" data-dismiss="modal">{{ trans('laravel-filemanager::lfm.btn-close') }}</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="fab"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="modal fade" id="notify" tabindex="-1" role="dialog" aria-hidden="true">
|
||||
<div class="modal-dialog modal-lg">
|
||||
<div class="modal-content">
|
||||
<div class="modal-body"></div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-secondary w-100" data-dismiss="modal">{{ trans('laravel-filemanager::lfm.btn-close') }}</button>
|
||||
<button type="button" class="btn btn-primary w-100" data-dismiss="modal">{{ trans('laravel-filemanager::lfm.btn-confirm') }}</button>
|
||||
<div class="modal fade" id="uploadModal" tabindex="-1" role="dialog" aria-labelledby="myModalLabel"
|
||||
aria-hidden="true">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h4 class="modal-title" id="myModalLabel">{{ trans('laravel-filemanager::lfm.title-upload') }}
|
||||
</h4>
|
||||
<button type="button" class="close" data-dismiss="modal" aria-label="Close"><span
|
||||
aia-hidden="true">×</span></button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<form action="{{ route('iqcontent.lfm.upload') }}" role='form' id='uploadForm'
|
||||
name='uploadForm' method='post' enctype='multipart/form-data' class="dropzone">
|
||||
<div class="form-group" id="attachment">
|
||||
<div class="controls text-center">
|
||||
<div class="input-group w-100">
|
||||
<a class="btn btn-primary w-100 text-white"
|
||||
id="upload-button">{{ trans('laravel-filemanager::lfm.message-choose') }}</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<input type='hidden' name='working_dir' id='working_dir'>
|
||||
<input type='hidden' name='type' id='type' value='{{ request('type') }}'>
|
||||
<input type='hidden' name='_token' value='{{ csrf_token() }}'>
|
||||
</form>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-secondary w-100"
|
||||
data-dismiss="modal">{{ trans('laravel-filemanager::lfm.btn-close') }}</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="modal fade" id="dialog" tabindex="-1" role="dialog" aria-hidden="true">
|
||||
<div class="modal-dialog modal-lg">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h4 class="modal-title"></h4>
|
||||
<div class="modal fade" id="notify" tabindex="-1" role="dialog" aria-hidden="true">
|
||||
<div class="modal-dialog modal-lg">
|
||||
<div class="modal-content">
|
||||
<div class="modal-body"></div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-secondary w-100"
|
||||
data-dismiss="modal">{{ trans('laravel-filemanager::lfm.btn-close') }}</button>
|
||||
<button type="button" class="btn btn-primary w-100"
|
||||
data-dismiss="modal">{{ trans('laravel-filemanager::lfm.btn-confirm') }}</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<input type="text" class="form-control">
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-secondary w-100" data-dismiss="modal">{{ trans('laravel-filemanager::lfm.btn-close') }}</button>
|
||||
<button type="button" class="btn btn-primary w-100" data-dismiss="modal">{{ trans('laravel-filemanager::lfm.btn-confirm') }}</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="carouselTemplate" class="d-none carousel slide bg-light" data-ride="carousel">
|
||||
<ol class="carousel-indicators">
|
||||
<li data-target="#previewCarousel" data-slide-to="0" class="active"></li>
|
||||
</ol>
|
||||
<div class="carousel-inner">
|
||||
<div class="carousel-item active">
|
||||
<a class="carousel-label"></a>
|
||||
<div class="carousel-image"></div>
|
||||
</div>
|
||||
<div class="modal fade" id="dialog" tabindex="-1" role="dialog" aria-hidden="true">
|
||||
<div class="modal-dialog modal-lg">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h4 class="modal-title"></h4>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<input type="text" class="form-control">
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-secondary w-100"
|
||||
data-dismiss="modal">{{ trans('laravel-filemanager::lfm.btn-close') }}</button>
|
||||
<button type="button" class="btn btn-primary w-100"
|
||||
data-dismiss="modal">{{ trans('laravel-filemanager::lfm.btn-confirm') }}</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<a class="carousel-control-prev" href="#previewCarousel" role="button" data-slide="prev">
|
||||
<div class="carousel-control-background" aria-hidden="true">
|
||||
<i class="fas fa-chevron-left"></i>
|
||||
</div>
|
||||
<span class="sr-only">Previous</span>
|
||||
</a>
|
||||
<a class="carousel-control-next" href="#previewCarousel" role="button" data-slide="next">
|
||||
<div class="carousel-control-background" aria-hidden="true">
|
||||
<i class="fas fa-chevron-right"></i>
|
||||
</div>
|
||||
<span class="sr-only">Next</span>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<script src="https://code.jquery.com/jquery-3.2.1.min.js"></script>
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/popper.js/1.12.3/umd/popper.min.js"></script>
|
||||
<script src="https://maxcdn.bootstrapcdn.com/bootstrap/4.1.0/js/bootstrap.min.js"></script>
|
||||
<script src="https://code.jquery.com/ui/1.12.1/jquery-ui.min.js"></script>
|
||||
<script src="{{ asset('vendor/laravel-filemanager/js/cropper.min.js') }}"></script>
|
||||
<script src="{{ asset('vendor/laravel-filemanager/js/dropzone.min.js') }}"></script>
|
||||
<script>
|
||||
var lang = {!! json_encode(trans('laravel-filemanager::lfm')) !!};
|
||||
var actions = [
|
||||
// {
|
||||
// name: 'use',
|
||||
// icon: 'check',
|
||||
// label: 'Confirm',
|
||||
// multiple: true
|
||||
// },
|
||||
{
|
||||
name: 'rename',
|
||||
icon: 'edit',
|
||||
label: lfm_lang['menu-rename'],
|
||||
multiple: false
|
||||
},
|
||||
{
|
||||
name: 'download',
|
||||
icon: 'download',
|
||||
label: lfm_lang['menu-download'],
|
||||
multiple: true
|
||||
},
|
||||
// {
|
||||
// name: 'preview',
|
||||
// icon: 'image',
|
||||
// label: lfm_lang['menu-view'],
|
||||
// multiple: true
|
||||
// },
|
||||
{
|
||||
name: 'move',
|
||||
icon: 'paste',
|
||||
label: lfm_lang['menu-move'],
|
||||
multiple: true
|
||||
},
|
||||
{
|
||||
name: 'resize',
|
||||
icon: 'arrows-alt',
|
||||
label: lfm_lang['menu-resize'],
|
||||
multiple: false
|
||||
},
|
||||
{
|
||||
name: 'crop',
|
||||
icon: 'crop',
|
||||
label: lfm_lang['menu-crop'],
|
||||
multiple: false
|
||||
},
|
||||
{
|
||||
name: 'trash',
|
||||
icon: 'trash',
|
||||
label: lfm_lang['menu-delete'],
|
||||
multiple: true
|
||||
},
|
||||
];
|
||||
<div id="carouselTemplate" class="d-none carousel slide bg-light" data-ride="carousel">
|
||||
<ol class="carousel-indicators">
|
||||
<li data-target="#previewCarousel" data-slide-to="0" class="active"></li>
|
||||
</ol>
|
||||
<div class="carousel-inner">
|
||||
<div class="carousel-item active">
|
||||
<a class="carousel-label"></a>
|
||||
<div class="carousel-image"></div>
|
||||
</div>
|
||||
</div>
|
||||
<a class="carousel-control-prev" href="#previewCarousel" role="button" data-slide="prev">
|
||||
<div class="carousel-control-background" aria-hidden="true">
|
||||
<i class="fas fa-chevron-left"></i>
|
||||
</div>
|
||||
<span class="sr-only">Previous</span>
|
||||
</a>
|
||||
<a class="carousel-control-next" href="#previewCarousel" role="button" data-slide="next">
|
||||
<div class="carousel-control-background" aria-hidden="true">
|
||||
<i class="fas fa-chevron-right"></i>
|
||||
</div>
|
||||
<span class="sr-only">Next</span>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
var sortings = [
|
||||
{
|
||||
by: 'alphabetic',
|
||||
icon: 'sort-alpha-down',
|
||||
label: lfm_lang['nav-sort-alphabetic']
|
||||
},
|
||||
{
|
||||
by: 'time',
|
||||
icon: 'sort-numeric-down',
|
||||
label: lfm_lang['nav-sort-time']
|
||||
}
|
||||
];
|
||||
</script>
|
||||
<script>{!! \File::get(base_path('vendor/iqcontent/laravel-filemanager/public/js/script.js')) !!}</script>
|
||||
{{-- Use the line below instead of the above if you need to cache the script. --}}
|
||||
{{-- <script src="{{ asset('vendor/laravel-filemanager/js/script.js') }}"></script> --}}
|
||||
<script>
|
||||
Dropzone.options.uploadForm = {
|
||||
paramName: "upload[]", // The name that will be used to transfer the file
|
||||
uploadMultiple: false,
|
||||
parallelUploads: 5,
|
||||
clickable: '#upload-button',
|
||||
dictDefaultMessage: '<i class="ion ion-ios-cloud-upload "></i>'+lfm_lang['message-drop'],
|
||||
init: function() {
|
||||
var _this = this; // For the closure
|
||||
this.on('success', function(file, response) {
|
||||
if (response == 'OK') {
|
||||
loadFolders();
|
||||
} else {
|
||||
this.defaultOptions.error(file, response.join('\n'));
|
||||
}
|
||||
});
|
||||
},
|
||||
headers: {
|
||||
'Authorization': 'Bearer ' + getUrlParam('token')
|
||||
},
|
||||
acceptedFiles: "{{ implode(',', $helper->availableMimeTypes()) }}",
|
||||
maxFilesize: ({{ $helper->maxUploadSize() }} / 1000)
|
||||
}
|
||||
</script>
|
||||
<script src="https://code.jquery.com/jquery-3.2.1.min.js"></script>
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/popper.js/1.12.3/umd/popper.min.js"></script>
|
||||
<script src="https://maxcdn.bootstrapcdn.com/bootstrap/4.1.0/js/bootstrap.min.js"></script>
|
||||
<script src="https://code.jquery.com/ui/1.12.1/jquery-ui.min.js"></script>
|
||||
<script src="{{ asset('vendor/laravel-filemanager/js/cropper.min.js') }}"></script>
|
||||
<script src="{{ asset('vendor/laravel-filemanager/js/dropzone.min.js') }}"></script>
|
||||
<script>
|
||||
var lang = {!! json_encode(trans('laravel-filemanager::lfm')) !!};
|
||||
var actions = [
|
||||
// {
|
||||
// name: 'use',
|
||||
// icon: 'check',
|
||||
// label: 'Confirm',
|
||||
// multiple: true
|
||||
// },
|
||||
{
|
||||
name: 'rename',
|
||||
icon: 'edit',
|
||||
label: lfm_lang['menu-rename'],
|
||||
multiple: false
|
||||
},
|
||||
{
|
||||
name: 'download',
|
||||
icon: 'download',
|
||||
label: lfm_lang['menu-download'],
|
||||
multiple: true
|
||||
},
|
||||
// {
|
||||
// name: 'preview',
|
||||
// icon: 'image',
|
||||
// label: lfm_lang['menu-view'],
|
||||
// multiple: true
|
||||
// },
|
||||
{
|
||||
name: 'move',
|
||||
icon: 'paste',
|
||||
label: lfm_lang['menu-move'],
|
||||
multiple: true
|
||||
},
|
||||
{
|
||||
name: 'resize',
|
||||
icon: 'arrows-alt',
|
||||
label: lfm_lang['menu-resize'],
|
||||
multiple: false
|
||||
},
|
||||
{
|
||||
name: 'crop',
|
||||
icon: 'crop',
|
||||
label: lfm_lang['menu-crop'],
|
||||
multiple: false
|
||||
},
|
||||
{
|
||||
name: 'trash',
|
||||
icon: 'trash',
|
||||
label: lfm_lang['menu-delete'],
|
||||
multiple: true
|
||||
},
|
||||
];
|
||||
|
||||
var sortings = [{
|
||||
by: 'alphabetic',
|
||||
icon: 'sort-alpha-down',
|
||||
label: lfm_lang['nav-sort-alphabetic']
|
||||
},
|
||||
{
|
||||
by: 'time',
|
||||
icon: 'sort-numeric-down',
|
||||
label: lfm_lang['nav-sort-time']
|
||||
}
|
||||
];
|
||||
</script>
|
||||
<script>
|
||||
{!! \File::get(base_path('vendor/iqcontent/laravel-filemanager/public/js/script.js')) !!}
|
||||
</script>
|
||||
{{-- Use the line below instead of the above if you need to cache the script. --}}
|
||||
{{-- <script src="{{ asset('vendor/laravel-filemanager/js/script.js') }}"></script> --}}
|
||||
<script>
|
||||
Dropzone.options.uploadForm = {
|
||||
paramName: "upload[]", // The name that will be used to transfer the file
|
||||
uploadMultiple: false,
|
||||
parallelUploads: 5,
|
||||
clickable: '#upload-button',
|
||||
dictDefaultMessage: '<i class="ion ion-ios-cloud-upload "></i>' + lfm_lang['message-drop'],
|
||||
init: function() {
|
||||
var _this = this; // For the closure
|
||||
this.on('success', function(file, response) {
|
||||
// Prüfe, ob die Antwort einen Fehler enthält
|
||||
if (response && response.error) {
|
||||
// Fehler in der erfolgreichen Antwort (sollte nicht passieren, aber zur Sicherheit)
|
||||
_this.emit('error', file, response);
|
||||
} else if (response == 'OK' || (response && response.url)) {
|
||||
// Erfolgreicher Upload
|
||||
loadFolders();
|
||||
|
||||
// Entferne die Datei nach kurzer Verzögerung
|
||||
setTimeout(function() {
|
||||
_this.removeFile(file);
|
||||
}, 2000);
|
||||
} else if (Array.isArray(response)) {
|
||||
// Alte Array-Antwort (sollte nicht mehr vorkommen)
|
||||
_this.emit('error', file, response.join('\n'));
|
||||
}
|
||||
});
|
||||
|
||||
this.on('error', function(file, errorMessage, xhr) {
|
||||
var message = errorMessage;
|
||||
|
||||
console.log('Dropzone Error Event:', {
|
||||
file: file.name,
|
||||
fileSize: file.size,
|
||||
errorMessage: errorMessage,
|
||||
xhr: xhr,
|
||||
xhrStatus: xhr ? xhr.status : 'no xhr',
|
||||
xhrResponse: xhr ? xhr.responseText : 'no response'
|
||||
});
|
||||
|
||||
// Behandle HTTP-Fehler-Responses
|
||||
if (xhr && xhr.status !== 200) {
|
||||
if (xhr.status === 413) {
|
||||
message =
|
||||
'Die Datei ist zu groß!\n\n' +
|
||||
'Der Server lehnt den Upload ab (HTTP 413 - Payload Too Large).\n\n' +
|
||||
'Aktuelles Server-Limit für Uploads:\n' +
|
||||
'- Nginx client_max_body_size: zu klein\n' +
|
||||
'- Empfohlen: mindestens 50MB\n\n' +
|
||||
'Bitte kontaktieren Sie den Administrator, um das Upload-Limit zu erhöhen.\n\n' +
|
||||
'Dateigröße: ' + (file.size / 1024 / 1024).toFixed(2) + ' MB';
|
||||
} else if (xhr.status === 0) {
|
||||
message =
|
||||
'Upload fehlgeschlagen: Keine Verbindung zum Server.\n\n' +
|
||||
'Mögliche Ursachen:\n' +
|
||||
'- Die Datei ist zu groß für den Server\n' +
|
||||
'- Netzwerkverbindung unterbrochen\n' +
|
||||
'- Server antwortet nicht\n\n' +
|
||||
'Dateigröße: ' + (file.size / 1024 / 1024).toFixed(2) + ' MB';
|
||||
} else {
|
||||
// Versuche die Fehlermeldung aus der Response zu extrahieren
|
||||
try {
|
||||
var response = JSON.parse(xhr.responseText);
|
||||
if (response && response.error && response.error.message) {
|
||||
message = response.error.message;
|
||||
} else if (response && response.message && response.message.trim() !== '') {
|
||||
message = response.message;
|
||||
} else {
|
||||
message = 'Server-Fehler (' + xhr.status + '): ' + xhr.statusText;
|
||||
}
|
||||
} catch (e) {
|
||||
message = 'Server-Fehler (' + xhr.status + '): ' + (xhr.statusText ||
|
||||
'Unbekannter Fehler');
|
||||
}
|
||||
}
|
||||
}
|
||||
// Wenn errorMessage ein Object ist, extrahiere die eigentliche Fehlermeldung
|
||||
else if (typeof errorMessage === 'object' && errorMessage !== null) {
|
||||
if (errorMessage.error && errorMessage.error.message) {
|
||||
// Backend-Fehlerformat: {error: {message: "..."}}
|
||||
message = errorMessage.error.message;
|
||||
} else if (errorMessage.message) {
|
||||
// Alternatives Format: {message: "..."}
|
||||
message = errorMessage.message;
|
||||
} else if (Array.isArray(errorMessage) && errorMessage.length > 0) {
|
||||
// Array von Fehlern
|
||||
message = errorMessage.join('\n');
|
||||
} else {
|
||||
// Fallback: JSON-String mit besserem Format
|
||||
try {
|
||||
message = 'Fehler: ' + JSON.stringify(errorMessage, null, 2);
|
||||
} catch (e) {
|
||||
message = 'Ein unbekannter Fehler ist aufgetreten.';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Aktualisiere die Fehlermeldung im Dropzone-Element
|
||||
if (file.previewElement) {
|
||||
var errorNode = file.previewElement.querySelector('[data-dz-errormessage]');
|
||||
if (errorNode) {
|
||||
errorNode.textContent = message;
|
||||
}
|
||||
}
|
||||
|
||||
// Zeige auch ein Alert
|
||||
alert('Upload-Fehler: ' + message);
|
||||
|
||||
console.error('Upload-Fehler (verarbeitet):', {
|
||||
file: file.name,
|
||||
message: message,
|
||||
originalError: errorMessage
|
||||
});
|
||||
});
|
||||
},
|
||||
headers: {
|
||||
'Authorization': 'Bearer ' + getUrlParam('token')
|
||||
},
|
||||
acceptedFiles: "{{ implode(',', $helper->availableMimeTypes()) }}",
|
||||
maxFilesize: ({{ $helper->maxUploadSize() }} / 1000)
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
|
|
|
|||
|
|
@ -236,7 +236,7 @@
|
|||
name="draft_item[{{ $booking_draft_item->id }}][adult]"
|
||||
id="draft_item_{{ $booking_draft_item->id }}_adult">
|
||||
<option value="">0</option>
|
||||
{!! HTMLHelper::getRangeOptions($booking_draft_item->adult, 10, '') !!}
|
||||
{!! HTMLHelper::getRangeOptions($booking_draft_item->adult, 20, '') !!}
|
||||
</select>
|
||||
{{ Form::text('draft_item[' . $booking_draft_item->id . '][price_adult]', $booking_draft_item->price_adult, ['placeholder' => __('Preis in €'), 'class' => 'form-control', 'id' => 'draft_item_' . $booking_draft_item->id . '_price_adult', 'maxlength' => 10]) }}
|
||||
<span class="input-group-append">
|
||||
|
|
@ -251,7 +251,7 @@
|
|||
name="draft_item[{{ $booking_draft_item->id }}][children]"
|
||||
id="draft_item_{{ $booking_draft_item->id }}_children">
|
||||
<option value="">0</option>
|
||||
{!! HTMLHelper::getRangeOptions($booking_draft_item->children, 10, '') !!}
|
||||
{!! HTMLHelper::getRangeOptions($booking_draft_item->children, 20, '') !!}
|
||||
</select>
|
||||
{{ Form::text('draft_item[' . $booking_draft_item->id . '][price_children]', $booking_draft_item->price_children, ['placeholder' => __('Preis in €'), 'class' => 'form-control', 'id' => 'draft_item_' . $booking_draft_item->id . '_price_children']) }}
|
||||
<span class="input-group-append">
|
||||
|
|
|
|||
|
|
@ -1,7 +1,6 @@
|
|||
@extends('layouts.layout-2')
|
||||
|
||||
@section('content')
|
||||
|
||||
<h4 class="font-weight-bold py-3 mb-1">
|
||||
{{ __('Reisenews') }}
|
||||
</h4>
|
||||
|
|
@ -13,43 +12,57 @@
|
|||
</div>
|
||||
<table class="datatables-news table table-striped table-bordered">
|
||||
<thead>
|
||||
<tr>
|
||||
<th style="max-width: 60px;"> </th>
|
||||
<th>{{__('Name')}}</th>
|
||||
<th>{{__('Parent')}}</th>
|
||||
<th>{{__('Date')}}</th>
|
||||
<th>{{__('sichtbar')}}</th>
|
||||
<th>#</th>
|
||||
</tr>
|
||||
<tr>
|
||||
<th style="max-width: 60px;"> </th>
|
||||
<th style="max-width: 80px;">Bild</th>
|
||||
<th>{{ __('Name') }}</th>
|
||||
<th>{{ __('Parent') }}</th>
|
||||
<th>{{ __('Date') }}</th>
|
||||
<th>{{ __('sichtbar') }}</th>
|
||||
<th>#</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@foreach($news as $value)
|
||||
<tr>
|
||||
<td>
|
||||
<a href="{{ route('cms_news_detail', [$value->id]) }}" class="btn icon-btn btn-sm btn-primary">
|
||||
<span class="fa fa-edit"></span>
|
||||
</a>
|
||||
</td>
|
||||
<td><a href="{{ route('cms_news_detail', [$value->id]) }}">{{ $value->title }}</a></td>
|
||||
<td>@if($value->parent) {{ $value->parent->title }} @else - Main No-Parent - @endif</td>
|
||||
<td data-sort="{{ $value->getDateRow() }}">{{ $value->date }}</td>
|
||||
@foreach ($news as $value)
|
||||
<tr>
|
||||
<td>
|
||||
<a href="{{ route('cms_news_detail', [$value->id]) }}"
|
||||
class="btn icon-btn btn-sm btn-primary">
|
||||
<span class="fa fa-edit"></span>
|
||||
</a>
|
||||
</td>
|
||||
<td>
|
||||
<img src="{{ $value->getImage('thumb_url') }}" alt="{{ $value->title }}" class="img-fluid"
|
||||
style="max-width: 80px;">
|
||||
</td>
|
||||
<td><a href="{{ route('cms_news_detail', [$value->id]) }}">{{ $value->title }}</a></td>
|
||||
<td>
|
||||
@if ($value->parent)
|
||||
{{ $value->parent->title }}
|
||||
@else
|
||||
- Main No-Parent -
|
||||
@endif
|
||||
</td>
|
||||
<td data-sort="{{ $value->getDateRow() }}">{{ $value->date }}</td>
|
||||
|
||||
<td data-sort="{{ $value->status }}">
|
||||
@if($value->status)
|
||||
<span class="badge badge-pill badge-success"><i class="fa fa-check"></i></span>
|
||||
@else
|
||||
<span class="badge badge-pill badge-danger"><i class="fa fa-times"></i></span>
|
||||
@endif
|
||||
</td>
|
||||
<td>
|
||||
@if($value->parent)
|
||||
<a class="text-danger" href="{{ route('cms_news_delete', [$value->id]) }}" onclick="return confirm('{{__('Wirklich löschen?')}}');"><i class="fa fa-trash-alt"></i></a>
|
||||
@else
|
||||
-
|
||||
@endif
|
||||
</td>
|
||||
</tr>
|
||||
@endforeach
|
||||
<td data-sort="{{ $value->status }}">
|
||||
@if ($value->status)
|
||||
<span class="badge badge-pill badge-success"><i class="fa fa-check"></i></span>
|
||||
@else
|
||||
<span class="badge badge-pill badge-danger"><i class="fa fa-times"></i></span>
|
||||
@endif
|
||||
</td>
|
||||
<td>
|
||||
@if ($value->parent)
|
||||
<a class="text-danger" href="{{ route('cms_news_delete', [$value->id]) }}"
|
||||
onclick="return confirm('{{ __('Wirklich löschen?') }}');"><i
|
||||
class="fa fa-trash-alt"></i></a>
|
||||
@else
|
||||
-
|
||||
@endif
|
||||
</td>
|
||||
</tr>
|
||||
@endforeach
|
||||
</tbody>
|
||||
</table>
|
||||
<div class="mt-4 col">
|
||||
|
|
@ -57,47 +70,45 @@
|
|||
</div>
|
||||
</div>
|
||||
<script>
|
||||
$( document ).ready(function() {
|
||||
$(document).ready(function() {
|
||||
$('.datatables-news').dataTable({
|
||||
"bLengthChange": false,
|
||||
"iDisplayLength": 50,
|
||||
"language": {
|
||||
"url": "/js/German.json"
|
||||
},
|
||||
initComplete: function () {
|
||||
this.api().columns(2).every( function () {
|
||||
initComplete: function() {
|
||||
this.api().columns(2).every(function() {
|
||||
var column = this;
|
||||
var title = $(column.header()).html();
|
||||
var select = $('<select class="selectpicker"><option value="">'+title+'</option></select>')
|
||||
.appendTo( $(column.header()).empty() )
|
||||
.on( 'change', function () {
|
||||
var select = $('<select class="selectpicker"><option value="">' +
|
||||
title + '</option></select>')
|
||||
.appendTo($(column.header()).empty())
|
||||
.on('change', function() {
|
||||
var val = $.fn.dataTable.util.escapeRegex(
|
||||
$(this).val()
|
||||
);
|
||||
|
||||
column
|
||||
.search( val ? '^'+val+'$' : '', true, false )
|
||||
.search(val ? '^' + val + '$' : '', true, false)
|
||||
.draw();
|
||||
} );
|
||||
});
|
||||
|
||||
column.data().unique().sort().each( function ( d, j ) {
|
||||
if(d !== ""){
|
||||
select.append( '<option value="'+d+'">'+d+'</option>' );
|
||||
column.data().unique().sort().each(function(d, j) {
|
||||
if (d !== "") {
|
||||
select.append('<option value="' + d + '">' + d +
|
||||
'</option>');
|
||||
}
|
||||
} );
|
||||
});
|
||||
|
||||
|
||||
} );
|
||||
});
|
||||
$('.selectpicker').selectpicker();
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
});
|
||||
|
||||
</script>
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
@endsection
|
||||
@endsection
|
||||
|
|
|
|||
|
|
@ -1,460 +1,673 @@
|
|||
@if (Auth::check())
|
||||
|
||||
|
||||
|
||||
@if(Auth::check())
|
||||
|
||||
<div id="layout-sidenav" class="{{ isset($layout_sidenav_horizontal) ? 'layout-sidenav-horizontal sidenav-horizontal container-p-x flex-grow-0' : 'layout-sidenav sidenav-vertical' }} sidenav bg-sidenav-theme">
|
||||
<div id="layout-sidenav"
|
||||
class="{{ isset($layout_sidenav_horizontal) ? 'layout-sidenav-horizontal sidenav-horizontal container-p-x flex-grow-0' : 'layout-sidenav sidenav-vertical' }} sidenav bg-sidenav-theme">
|
||||
|
||||
<!-- Inner -->
|
||||
<ul class="sidenav-inner{{ empty($layout_sidenav_horizontal) ? ' py-1' : '' }}">
|
||||
|
||||
<li class="sidenav-item{{ Request::is('home') ? ' active' : '' }}">
|
||||
<a href="{{ route('home') }}" class="sidenav-link"><i class="sidenav-icon ion ion-md-apps"></i><div>{{__('Home')}}</div></a>
|
||||
<a href="{{ route('home') }}" class="sidenav-link"><i class="sidenav-icon ion ion-md-apps"></i>
|
||||
<div>{{ __('Home') }}</div>
|
||||
</a>
|
||||
</li>
|
||||
|
||||
@if(Auth::user()->isPermission('my-dat'))
|
||||
@if (Auth::user()->isPermission('my-dat'))
|
||||
<li class="sidenav-item{{ Request::is('user/edit') ? ' active' : '' }}">
|
||||
<a href="{{ route('user_edit') }}" class="sidenav-link"><i class="sidenav-icon ion ion-ios-create"></i><div>{{ __('Your Data') }}</div></a>
|
||||
<a href="{{ route('user_edit') }}" class="sidenav-link"><i
|
||||
class="sidenav-icon ion ion-ios-create"></i>
|
||||
<div>{{ __('Your Data') }}</div>
|
||||
</a>
|
||||
</li>
|
||||
@endif
|
||||
|
||||
@if(Auth::user()->isAdmin())
|
||||
@if(Auth::user()->isPermission('crm'))
|
||||
@if (Auth::user()->isAdmin())
|
||||
@if (Auth::user()->isPermission('crm'))
|
||||
<li class="sidenav-divider mb-1"></li>
|
||||
<li class="sidenav-header small font-weight-semibold">ADMIN CRM</li>
|
||||
@if(Auth::user()->isPermission('crm-tp'))
|
||||
<li class="sidenav-item{{ Request::is(['travel/*', 'drafts', 'draft/*', 'travel_content', 'travel_content/*']) ? ' open' : '' }}">
|
||||
<a href="javascript:void(0)" class="sidenav-link sidenav-toggle">
|
||||
<i class="sidenav-icon ion ion-ios-airplane"></i>
|
||||
<div>Reiseprogramme</div>
|
||||
</a>
|
||||
<ul class="sidenav-menu">
|
||||
@if(Auth::user()->isPermission('crm-tp-pr'))
|
||||
<li class="sidenav-item{{ Request::is('travel/*') ? ' active' : '' }}">
|
||||
<a href="{{ route('travel_programs') }}" class="sidenav-link"><i class="sidenav-icon ion ion-md-airplane"></i><div>Programme</div></a>
|
||||
</li>
|
||||
@endif
|
||||
@if(Auth::user()->isPermission('crm-tp-dr'))
|
||||
<li class="sidenav-item{{ Request::is(['drafts','draft/*']) ? ' active' : '' }}">
|
||||
<a href="{{ route('drafts') }}" class="sidenav-link"><i class="sidenav-icon ion ion-md-paper"></i><div>Vorlagen</div></a>
|
||||
</li>
|
||||
@endif
|
||||
@if(Auth::user()->isPermission('crm-tp-tc'))
|
||||
<li class="sidenav-item{{ Request::is(['travel_content', 'travel_content/*']) ? ' active' : '' }}">
|
||||
<a href="{{ route('travel_content') }}" class="sidenav-link"><i class="sidenav-icon ion ion-md-map"></i><div>Inhalte</div></a>
|
||||
</li>
|
||||
@endif
|
||||
</ul>
|
||||
</li>
|
||||
@if (Auth::user()->isPermission('crm-tp'))
|
||||
<li
|
||||
class="sidenav-item{{ Request::is(['travel/*', 'drafts', 'draft/*', 'travel_content', 'travel_content/*']) ? ' open' : '' }}">
|
||||
<a href="javascript:void(0)" class="sidenav-link sidenav-toggle">
|
||||
<i class="sidenav-icon ion ion-ios-airplane"></i>
|
||||
<div>Reiseprogramme</div>
|
||||
</a>
|
||||
<ul class="sidenav-menu">
|
||||
@if (Auth::user()->isPermission('crm-tp-pr'))
|
||||
<li class="sidenav-item{{ Request::is('travel/*') ? ' active' : '' }}">
|
||||
<a href="{{ route('travel_programs') }}" class="sidenav-link"><i
|
||||
class="sidenav-icon ion ion-md-airplane"></i>
|
||||
<div>Programme</div>
|
||||
</a>
|
||||
</li>
|
||||
@endif
|
||||
@if (Auth::user()->isPermission('crm-tp-dr'))
|
||||
<li class="sidenav-item{{ Request::is(['drafts', 'draft/*']) ? ' active' : '' }}">
|
||||
<a href="{{ route('drafts') }}" class="sidenav-link"><i
|
||||
class="sidenav-icon ion ion-md-paper"></i>
|
||||
<div>Vorlagen</div>
|
||||
</a>
|
||||
</li>
|
||||
@endif
|
||||
@if (Auth::user()->isPermission('crm-tp-tc'))
|
||||
<li
|
||||
class="sidenav-item{{ Request::is(['travel_content', 'travel_content/*']) ? ' active' : '' }}">
|
||||
<a href="{{ route('travel_content') }}" class="sidenav-link"><i
|
||||
class="sidenav-icon ion ion-md-map"></i>
|
||||
<div>Inhalte</div>
|
||||
</a>
|
||||
</li>
|
||||
@endif
|
||||
|
||||
</ul>
|
||||
</li>
|
||||
@endif
|
||||
@if(Auth::user()->isPermission('crm-bo'))
|
||||
<li class="sidenav-item{{ Request::is(['requests', 'bookings', 'booking/*', 'leads', 'lead/*', 'customers', 'customer/*','customer_mails', 'customer_mail/*']) ? ' open' : '' }}">
|
||||
<a href="javascript:void(0)" class="sidenav-link sidenav-toggle">
|
||||
<i class="sidenav-icon ion ion-md-bed"></i>
|
||||
<div>Buchungen</div>
|
||||
</a>
|
||||
<ul class="sidenav-menu">
|
||||
@if(Auth::user()->isPermission('crm-bo-re'))
|
||||
<li class="sidenav-item{{ Request::is('requests') ? ' active' : '' }}">
|
||||
<a href="{{ route('requests') }}" class="sidenav-link"><i class="sidenav-icon ion ion-md-search"></i><div>Buchungen</div></a>
|
||||
</li>
|
||||
@endif
|
||||
{{--
|
||||
@if(Auth::user()->isPermission('crm-bo-bo'))
|
||||
@if (Auth::user()->isPermission('crm-bo'))
|
||||
<li
|
||||
class="sidenav-item{{ Request::is(['requests', 'bookings', 'booking/*', 'leads', 'lead/*', 'customers', 'customer/*', 'customer_mails', 'customer_mail/*']) ? ' open' : '' }}">
|
||||
<a href="javascript:void(0)" class="sidenav-link sidenav-toggle">
|
||||
<i class="sidenav-icon ion ion-md-bed"></i>
|
||||
<div>Buchungen</div>
|
||||
</a>
|
||||
<ul class="sidenav-menu">
|
||||
@if (Auth::user()->isPermission('crm-bo-re'))
|
||||
<li class="sidenav-item{{ Request::is('requests') ? ' active' : '' }}">
|
||||
<a href="{{ route('requests') }}" class="sidenav-link"><i
|
||||
class="sidenav-icon ion ion-md-search"></i>
|
||||
<div>Buchungen</div>
|
||||
</a>
|
||||
</li>
|
||||
@endif
|
||||
{{--
|
||||
@if (Auth::user()->isPermission('crm-bo-bo'))
|
||||
<li class="sidenav-item{{ Request::is(['bookings', 'booking/*']) ? ' active' : '' }}">
|
||||
<a href="{{ route('bookings') }}" class="sidenav-link"><i class="sidenav-icon ion ion-ios-bed"></i><div>Buchungen</div></a>
|
||||
</li>
|
||||
@endif
|
||||
--}}
|
||||
@if(Auth::user()->isPermission('crm-bo-le'))
|
||||
<li class="sidenav-item{{ Request::is(['leads', 'lead/*']) ? ' active' : '' }}">
|
||||
<a href="{{ route('leads') }}" class="sidenav-link"><i class="sidenav-icon ion ion-ios-contact"></i><div>Anfragen</div></a>
|
||||
</li>
|
||||
@endif
|
||||
@if(Auth::user()->isPermission('crm-bo-cu'))
|
||||
<li class="sidenav-item{{ Request::is(['customers', 'customer/*']) ? ' active' : '' }}">
|
||||
<a href="{{ route('customers') }}" class="sidenav-link"><i class="sidenav-icon ion ion-ios-people"></i><div>Kunden</div></a>
|
||||
</li>
|
||||
@endif
|
||||
</ul>
|
||||
</li>
|
||||
@if (Auth::user()->isPermission('crm-bo-le'))
|
||||
<li class="sidenav-item{{ Request::is(['leads', 'lead/*']) ? ' active' : '' }}">
|
||||
<a href="{{ route('leads') }}" class="sidenav-link"><i
|
||||
class="sidenav-icon ion ion-ios-contact"></i>
|
||||
<div>Anfragen</div>
|
||||
</a>
|
||||
</li>
|
||||
@endif
|
||||
@if (Auth::user()->isPermission('crm-bo-cu'))
|
||||
<li
|
||||
class="sidenav-item{{ Request::is(['customers', 'customer/*']) ? ' active' : '' }}">
|
||||
<a href="{{ route('customers') }}" class="sidenav-link"><i
|
||||
class="sidenav-icon ion ion-ios-people"></i>
|
||||
<div>Kunden</div>
|
||||
</a>
|
||||
</li>
|
||||
@endif
|
||||
</ul>
|
||||
</li>
|
||||
@endif
|
||||
@if(Auth::user()->isPermission('crm-cm'))
|
||||
<li class="sidenav-item{{ Request::is(['travel_users', 'travel_user/*', 'travel_user_booking_fewos', 'travel_user_booking_fewo/*']) ? ' open' : '' }}">
|
||||
<a href="javascript:void(0)" class="sidenav-link sidenav-toggle">
|
||||
<i class="sidenav-icon ion ion-ios-home"></i>
|
||||
<div>Buchungen FeWo</div>
|
||||
</a>
|
||||
<ul class="sidenav-menu">
|
||||
@if(Auth::user()->isPermission('crm-cm-cf'))
|
||||
<li class="sidenav-item{{ Request::is('travel_users') ? ' active' : '' }} {{ Request::is('travel_user/*') ? ' active' : '' }}">
|
||||
<a href="{{ route('travel_users') }}" class="sidenav-link"><i class="sidenav-icon ion ion-ios-people"></i><div>Kunden (FeWo)</div></a>
|
||||
</li>
|
||||
@endif
|
||||
@if(Auth::user()->isPermission('crm-cm-bf'))
|
||||
<li class="sidenav-item{{ Request::is('travel_user_booking_fewos') ? ' active' : '' }} {{ Request::is('travel_user_booking_fewo/*') ? ' active' : '' }}">
|
||||
<a href="{{ route('travel_user_booking_fewos') }}" class="sidenav-link"><i class="sidenav-icon ion ion-ios-bed"></i><div>Buchungen (FeWo)</div></a>
|
||||
</li>
|
||||
@endif
|
||||
</ul>
|
||||
</li>
|
||||
@if (Auth::user()->isPermission('crm-cm'))
|
||||
<li
|
||||
class="sidenav-item{{ Request::is(['travel_users', 'travel_user/*', 'travel_user_booking_fewos', 'travel_user_booking_fewo/*']) ? ' open' : '' }}">
|
||||
<a href="javascript:void(0)" class="sidenav-link sidenav-toggle">
|
||||
<i class="sidenav-icon ion ion-ios-home"></i>
|
||||
<div>Buchungen FeWo</div>
|
||||
</a>
|
||||
<ul class="sidenav-menu">
|
||||
@if (Auth::user()->isPermission('crm-cm-cf'))
|
||||
<li
|
||||
class="sidenav-item{{ Request::is('travel_users') ? ' active' : '' }} {{ Request::is('travel_user/*') ? ' active' : '' }}">
|
||||
<a href="{{ route('travel_users') }}" class="sidenav-link"><i
|
||||
class="sidenav-icon ion ion-ios-people"></i>
|
||||
<div>Kunden (FeWo)</div>
|
||||
</a>
|
||||
</li>
|
||||
@endif
|
||||
@if (Auth::user()->isPermission('crm-cm-bf'))
|
||||
<li
|
||||
class="sidenav-item{{ Request::is('travel_user_booking_fewos') ? ' active' : '' }} {{ Request::is('travel_user_booking_fewo/*') ? ' active' : '' }}">
|
||||
<a href="{{ route('travel_user_booking_fewos') }}" class="sidenav-link"><i
|
||||
class="sidenav-icon ion ion-ios-bed"></i>
|
||||
<div>Buchungen (FeWo)</div>
|
||||
</a>
|
||||
</li>
|
||||
@endif
|
||||
</ul>
|
||||
</li>
|
||||
@endif
|
||||
|
||||
@if(Auth::user()->isPermission('crm-mail'))
|
||||
<li class="sidenav-item{{ Request::is(['mail/leads', 'mail/bookings', 'mail/booking_fewos']) ? ' open' : '' }}">
|
||||
<a href="javascript:void(0)" class="sidenav-link sidenav-toggle">
|
||||
<i class="sidenav-icon ion ion-ios-mail"></i>
|
||||
<div>E-Mails</div>
|
||||
</a>
|
||||
<ul class="sidenav-menu">
|
||||
@if(Auth::user()->isPermission('crm-mail-le'))
|
||||
<li class="sidenav-item{{ Request::is(['mail/leads', 'mail/lead/*']) ? ' active' : '' }}">
|
||||
<a href="{{ route('mail_leads') }}" class="sidenav-link"><i class="sidenav-icon ion ion-ios-people"></i><div>Anfragen </div></a>
|
||||
</li>
|
||||
@endif
|
||||
@if(Auth::user()->isPermission('crm-mail-bo'))
|
||||
<li class="sidenav-item{{ Request::is(['mail/bookings', 'mail/booking/*']) ? ' active' : '' }}">
|
||||
<a href="{{ route('mail_bookings') }}" class="sidenav-link"><i class="sidenav-icon ion ion-md-bed"></i><div>Buchungen</div></a>
|
||||
</li>
|
||||
@endif
|
||||
@if(Auth::user()->isPermission('crm-mail-bf'))
|
||||
<li class="sidenav-item{{ Request::is(['mail/booking_fewos', 'mail/booking_fewo/*']) ? ' active' : '' }}">
|
||||
<a href="{{ route('mail_booking_fewos') }}" class="sidenav-link"><i class="sidenav-icon ion ion-ios-home"></i><div>Buchungen (FeWo)</div></a>
|
||||
</li>
|
||||
@endif
|
||||
</ul>
|
||||
</li>
|
||||
@if (Auth::user()->isPermission('crm-mail'))
|
||||
<li
|
||||
class="sidenav-item{{ Request::is(['mail/leads', 'mail/bookings', 'mail/booking_fewos']) ? ' open' : '' }}">
|
||||
<a href="javascript:void(0)" class="sidenav-link sidenav-toggle">
|
||||
<i class="sidenav-icon ion ion-ios-mail"></i>
|
||||
<div>E-Mails</div>
|
||||
</a>
|
||||
<ul class="sidenav-menu">
|
||||
@if (Auth::user()->isPermission('crm-mail-le'))
|
||||
<li
|
||||
class="sidenav-item{{ Request::is(['mail/leads', 'mail/lead/*']) ? ' active' : '' }}">
|
||||
<a href="{{ route('mail_leads') }}" class="sidenav-link"><i
|
||||
class="sidenav-icon ion ion-ios-people"></i>
|
||||
<div>Anfragen </div>
|
||||
</a>
|
||||
</li>
|
||||
@endif
|
||||
@if (Auth::user()->isPermission('crm-mail-bo'))
|
||||
<li
|
||||
class="sidenav-item{{ Request::is(['mail/bookings', 'mail/booking/*']) ? ' active' : '' }}">
|
||||
<a href="{{ route('mail_bookings') }}" class="sidenav-link"><i
|
||||
class="sidenav-icon ion ion-md-bed"></i>
|
||||
<div>Buchungen</div>
|
||||
</a>
|
||||
</li>
|
||||
@endif
|
||||
@if (Auth::user()->isPermission('crm-mail-bf'))
|
||||
<li
|
||||
class="sidenav-item{{ Request::is(['mail/booking_fewos', 'mail/booking_fewo/*']) ? ' active' : '' }}">
|
||||
<a href="{{ route('mail_booking_fewos') }}" class="sidenav-link"><i
|
||||
class="sidenav-icon ion ion-ios-home"></i>
|
||||
<div>Buchungen (FeWo)</div>
|
||||
</a>
|
||||
</li>
|
||||
@endif
|
||||
</ul>
|
||||
</li>
|
||||
@endif
|
||||
|
||||
@if(Auth::user()->isPermission('crm-iq-tl'))
|
||||
<li class="sidenav-item{{ Request::is(['iq/travel/*']) ? ' open' : '' }}">
|
||||
<a href="javascript:void(0)" class="sidenav-link sidenav-toggle">
|
||||
<i class="sidenav-icon ion ion-md-apps"></i>
|
||||
<div>Reisebausteine</div>
|
||||
</a>
|
||||
<ul class="sidenav-menu">
|
||||
@if(Auth::user()->isPermission('crm-iq-tl-pro'))
|
||||
<li class="sidenav-item{{ Request::is(['iq/travel/programms', 'iq/travel/programm/*']) ? ' active' : '' }}">
|
||||
<a href="{{ route('iq_travel_programms') }}" class="sidenav-link"><i class="sidenav-icon ion ion-md-airplane"></i><div>Programm</div></a>
|
||||
</li>
|
||||
@endif
|
||||
@if(Auth::user()->isPermission('crm-iq-tl-gp'))
|
||||
<li class="sidenav-item{{ Request::is(['iq/travel/groups', 'iq/travel/group/*']) ? ' active' : '' }}">
|
||||
<a href="{{ route('iq_travel_groups') }}" class="sidenav-link"><i class="sidenav-icon ion ion-md-grid"></i><div>Gruppe</div></a>
|
||||
</li>
|
||||
@endif
|
||||
@if(Auth::user()->isPermission('crm-iq-tl-it'))
|
||||
<li class="sidenav-item{{ Request::is(['iq/travel/items', 'iq/travel/item/*']) ? ' active' : '' }}">
|
||||
<a href="{{ route('iq_travel_items') }}" class="sidenav-link"><i class="sidenav-icon ion ion-md-cube"></i><div>Baustein</div></a>
|
||||
</li>
|
||||
@endif
|
||||
</ul>
|
||||
</li>
|
||||
@if (Auth::user()->isPermission('crm-iq-tl'))
|
||||
<li class="sidenav-item{{ Request::is(['iq/travel/*']) ? ' open' : '' }}">
|
||||
<a href="javascript:void(0)" class="sidenav-link sidenav-toggle">
|
||||
<i class="sidenav-icon ion ion-md-apps"></i>
|
||||
<div>Reisebausteine</div>
|
||||
</a>
|
||||
<ul class="sidenav-menu">
|
||||
@if (Auth::user()->isPermission('crm-iq-tl-pro'))
|
||||
<li
|
||||
class="sidenav-item{{ Request::is(['iq/travel/programms', 'iq/travel/programm/*']) ? ' active' : '' }}">
|
||||
<a href="{{ route('iq_travel_programms') }}" class="sidenav-link"><i
|
||||
class="sidenav-icon ion ion-md-airplane"></i>
|
||||
<div>Programm</div>
|
||||
</a>
|
||||
</li>
|
||||
@endif
|
||||
@if (Auth::user()->isPermission('crm-iq-tl-gp'))
|
||||
<li
|
||||
class="sidenav-item{{ Request::is(['iq/travel/groups', 'iq/travel/group/*']) ? ' active' : '' }}">
|
||||
<a href="{{ route('iq_travel_groups') }}" class="sidenav-link"><i
|
||||
class="sidenav-icon ion ion-md-grid"></i>
|
||||
<div>Gruppe</div>
|
||||
</a>
|
||||
</li>
|
||||
@endif
|
||||
@if (Auth::user()->isPermission('crm-iq-tl-it'))
|
||||
<li
|
||||
class="sidenav-item{{ Request::is(['iq/travel/items', 'iq/travel/item/*']) ? ' active' : '' }}">
|
||||
<a href="{{ route('iq_travel_items') }}" class="sidenav-link"><i
|
||||
class="sidenav-icon ion ion-md-cube"></i>
|
||||
<div>Baustein</div>
|
||||
</a>
|
||||
</li>
|
||||
@endif
|
||||
</ul>
|
||||
</li>
|
||||
@endif
|
||||
|
||||
@endif
|
||||
@if(Auth::user()->isPermission('crm-old-cm'))
|
||||
<li class="sidenav-divider mb-1"></li>
|
||||
<li class="sidenav-header small font-weight-semibold">ADMIN CRM altes System</li>
|
||||
@if (Auth::user()->isPermission('crm-old-cm'))
|
||||
<li class="sidenav-divider mb-1"></li>
|
||||
<li class="sidenav-header small font-weight-semibold">ADMIN CRM altes System</li>
|
||||
|
||||
<li class="sidenav-item">
|
||||
<a href="javascript:void(0)" class="sidenav-link sidenav-toggle">
|
||||
<i class="sidenav-icon ion ion-ios-people"></i>
|
||||
<div>Kundenverwaltung</div>
|
||||
</a>
|
||||
<ul class="sidenav-menu">
|
||||
<li class="sidenav-item">
|
||||
<a href="{{ make_old_url('/index.php/customers') }}" class="sidenav-link">
|
||||
<div>Kunden</div>
|
||||
</a>
|
||||
</li>
|
||||
<li class="sidenav-item">
|
||||
<a href="{{ make_old_url('/index.php/leads') }}" class="sidenav-link">
|
||||
<div>Anfragen</div>
|
||||
</a>
|
||||
</li>
|
||||
<li class="sidenav-item">
|
||||
<a href="{{ make_old_url('/index.php/bookings') }}" class="sidenav-link">
|
||||
<div>Buchungen</div>
|
||||
</a>
|
||||
</li>
|
||||
<li class="sidenav-item">
|
||||
<a href="{{ make_old_url('/index.php/payments') }}" class="sidenav-link">
|
||||
<div>Zahlungen</div>
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</li>
|
||||
<li class="sidenav-item">
|
||||
<a href="javascript:void(0)" class="sidenav-link sidenav-toggle">
|
||||
<i class="sidenav-icon ion ion-ios-people"></i>
|
||||
<div>Kundenverwaltung</div>
|
||||
</a>
|
||||
<ul class="sidenav-menu">
|
||||
<li class="sidenav-item">
|
||||
<a href="{{ make_old_url('/index.php/customers') }}" class="sidenav-link">
|
||||
<div>Kunden</div>
|
||||
</a>
|
||||
</li>
|
||||
<li class="sidenav-item">
|
||||
<a href="{{ make_old_url('/index.php/leads') }}" class="sidenav-link">
|
||||
<div>Anfragen</div>
|
||||
</a>
|
||||
</li>
|
||||
<li class="sidenav-item">
|
||||
<a href="{{ make_old_url('/index.php/bookings') }}" class="sidenav-link">
|
||||
<div>Buchungen</div>
|
||||
</a>
|
||||
</li>
|
||||
<li class="sidenav-item">
|
||||
<a href="{{ make_old_url('/index.php/payments') }}" class="sidenav-link">
|
||||
<div>Zahlungen</div>
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</li>
|
||||
@endif
|
||||
@if(Auth::user()->isPermission('cms'))
|
||||
@if (Auth::user()->isPermission('cms'))
|
||||
<li class="sidenav-divider mb-1"></li>
|
||||
<li class="sidenav-header small font-weight-semibold">ADMIN CMS</li>
|
||||
@if(Auth::user()->isPermission('cms-iq-assets'))
|
||||
<li class="sidenav-item{{ Request::is('iq/content/assets') ? ' active' : '' }}">
|
||||
<a href="{{ route('iq_content_assets') }}" class="sidenav-link"><i class="sidenav-icon ion ion-ios-images"></i><div>Medien</div></a>
|
||||
@if (Auth::user()->isPermission('cms-iq-assets'))
|
||||
<li class="sidenav-item{{ Request::is('iq/content/assets') ? ' active' : '' }}">
|
||||
<a href="{{ route('iq_content_assets') }}" class="sidenav-link"><i
|
||||
class="sidenav-icon ion ion-ios-images"></i>
|
||||
<div>Medien</div>
|
||||
</a>
|
||||
</li>
|
||||
@endif
|
||||
@endif
|
||||
|
||||
@if(Auth::user()->isPermission('cms-tg'))
|
||||
<li class="sidenav-item{{ Request::is('cms/travel_guide/*') ? ' open' : '' }} {{ Request::is('iq/content/tree/*') ? ' open' : '' }}">
|
||||
@if (Auth::user()->isPermission('cms-tg'))
|
||||
<li
|
||||
class="sidenav-item{{ Request::is('cms/travel_guide/*') ? ' open' : '' }} {{ Request::is('iq/content/tree/*') ? ' open' : '' }}">
|
||||
<a href="javascript:void(0)" class="sidenav-link sidenav-toggle">
|
||||
<i class="sidenav-icon ion ion-ios-journal"></i>
|
||||
<div>Reiseführer</div>
|
||||
</a>
|
||||
<ul class="sidenav-menu">
|
||||
<li class="sidenav-item{{ Request::is('iq/content/tree/*') ? ' active' : '' }}">
|
||||
<a href="{{ route('iq_content_tree_index') }}" class="sidenav-link"><i class="sidenav-icon ion ion-md-list"></i><div>Reiseführer Tree</div></a>
|
||||
<a href="{{ route('iq_content_tree_index') }}" class="sidenav-link"><i
|
||||
class="sidenav-icon ion ion-md-list"></i>
|
||||
<div>Reiseführer Tree</div>
|
||||
</a>
|
||||
</li>
|
||||
{{--
|
||||
<li class="sidenav-item{{ Request::is('cms/travel_guide/page') ? ' active' : '' }}">
|
||||
<a href="{{ route('cms_travel_guide_page') }}" class="sidenav-link"><i class="sidenav-icon ion ion-md-list-box"></i><div>Reiseführer Seiten</div></a>
|
||||
</li>
|
||||
--}}
|
||||
<li class="sidenav-item{{ Request::is('cms/travel_guide/content') ? ' active' : '' }}">
|
||||
<a href="{{ route('cms_travel_guide_content') }}" class="sidenav-link"><i class="sidenav-icon ion ion-md-paper"></i><div>Reiseführer Inhalte</div></a>
|
||||
<li
|
||||
class="sidenav-item{{ Request::is('cms/travel_guide/content') ? ' active' : '' }}">
|
||||
<a href="{{ route('cms_travel_guide_content') }}" class="sidenav-link"><i
|
||||
class="sidenav-icon ion ion-md-paper"></i>
|
||||
<div>Reiseführer Inhalte</div>
|
||||
</a>
|
||||
</li>
|
||||
<li class="sidenav-item{{ Request::is('cms/travel_guide/test') ? ' active' : '' }}">
|
||||
<a href="{{ route('cms_travel_guide_test') }}" class="sidenav-link"><i class="sidenav-icon ion ion-ios-alert "></i><div>TEST</div></a>
|
||||
<a href="{{ route('cms_travel_guide_test') }}" class="sidenav-link"><i
|
||||
class="sidenav-icon ion ion-ios-alert "></i>
|
||||
<div>TEST</div>
|
||||
</a>
|
||||
</li>
|
||||
|
||||
|
||||
</ul>
|
||||
</li>
|
||||
@endif
|
||||
@if(Auth::user()->isPermission('cms-book'))
|
||||
<li class="sidenav-item{{ Request::is('cms/booking/*') ? ' open' : '' }}">
|
||||
<a href="javascript:void(0)" class="sidenav-link sidenav-toggle">
|
||||
<i class="sidenav-icon ion ion-md-bed"></i>
|
||||
<div>Buchungen</div>
|
||||
</a>
|
||||
<ul class="sidenav-menu">
|
||||
<li class="sidenav-item{{ Request::is(['cms/booking/all', 'cms/booking/all/*']) ? ' active' : '' }}">
|
||||
<a href="{{ route('cms_booking_all') }}" class="sidenav-link"><i class="sidenav-icon ion ion-ios-paper"></i><div>PDF Vorlagen</div></a>
|
||||
</li>
|
||||
<li class="sidenav-item{{ Request::is(['cms/booking/content', 'cms/booking/content/*']) ? ' active' : '' }}">
|
||||
<a href="{{ route('cms_booking_content') }}" class="sidenav-link"><i class="sidenav-icon ion ion-md-paper"></i><div>PDF Inhalte</div></a>
|
||||
</li>
|
||||
</ul>
|
||||
</li>
|
||||
@if (Auth::user()->isPermission('cms-book'))
|
||||
<li class="sidenav-item{{ Request::is('cms/booking/*') ? ' open' : '' }}">
|
||||
<a href="javascript:void(0)" class="sidenav-link sidenav-toggle">
|
||||
<i class="sidenav-icon ion ion-md-bed"></i>
|
||||
<div>Buchungen</div>
|
||||
</a>
|
||||
<ul class="sidenav-menu">
|
||||
<li
|
||||
class="sidenav-item{{ Request::is(['cms/booking/all', 'cms/booking/all/*']) ? ' active' : '' }}">
|
||||
<a href="{{ route('cms_booking_all') }}" class="sidenav-link"><i
|
||||
class="sidenav-icon ion ion-ios-paper"></i>
|
||||
<div>PDF Vorlagen</div>
|
||||
</a>
|
||||
</li>
|
||||
<li
|
||||
class="sidenav-item{{ Request::is(['cms/booking/content', 'cms/booking/content/*']) ? ' active' : '' }}">
|
||||
<a href="{{ route('cms_booking_content') }}" class="sidenav-link"><i
|
||||
class="sidenav-icon ion ion-md-paper"></i>
|
||||
<div>PDF Inhalte</div>
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</li>
|
||||
@endif
|
||||
@if(Auth::user()->isPermission('cms-fewo'))
|
||||
@if (Auth::user()->isPermission('cms-fewo'))
|
||||
<li class="sidenav-item{{ Request::is('cms/fewo/*') ? ' open' : '' }}">
|
||||
<a href="javascript:void(0)" class="sidenav-link sidenav-toggle">
|
||||
<i class="sidenav-icon ion ion-ios-home"></i>
|
||||
<div>FeWo</div>
|
||||
</a>
|
||||
<ul class="sidenav-menu">
|
||||
<li class="sidenav-item{{ Request::is(['cms/fewo/all', 'cms/fewo/all/*']) ? ' active' : '' }}">
|
||||
<a href="{{ route('cms_fewo_all') }}" class="sidenav-link"><i class="sidenav-icon ion ion-ios-paper"></i><div>FeWo Vorlage</div></a>
|
||||
<li
|
||||
class="sidenav-item{{ Request::is(['cms/fewo/all', 'cms/fewo/all/*']) ? ' active' : '' }}">
|
||||
<a href="{{ route('cms_fewo_all') }}" class="sidenav-link"><i
|
||||
class="sidenav-icon ion ion-ios-paper"></i>
|
||||
<div>FeWo Vorlage</div>
|
||||
</a>
|
||||
</li>
|
||||
<li class="sidenav-item{{ Request::is(['cms/fewo/content', 'cms/fewo/content/*']) ? ' active' : '' }}">
|
||||
<a href="{{ route('cms_fewo_content') }}" class="sidenav-link"><i class="sidenav-icon ion ion-md-paper"></i><div>FeWo Inhalte</div></a>
|
||||
<li
|
||||
class="sidenav-item{{ Request::is(['cms/fewo/content', 'cms/fewo/content/*']) ? ' active' : '' }}">
|
||||
<a href="{{ route('cms_fewo_content') }}" class="sidenav-link"><i
|
||||
class="sidenav-icon ion ion-md-paper"></i>
|
||||
<div>FeWo Inhalte</div>
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</li>
|
||||
@endif
|
||||
@if(Auth::user()->isPermission('cms-fb'))
|
||||
<li class="sidenav-item{{ Request::is('cms/feedback', 'cms/feedback/*') ? ' active' : '' }}">
|
||||
<a href="{{ route('cms_feedback') }}" class="sidenav-link"><i class="sidenav-icon ion ion-md-chatboxes"></i><div>Feedback</div></a>
|
||||
</li>
|
||||
@endif
|
||||
@if(Auth::user()->isPermission('cms-nw'))
|
||||
<li class="sidenav-item{{ Request::is('cms/news', 'cms/news/*') ? ' active' : '' }}">
|
||||
<a href="{{ route('cms_news') }}" class="sidenav-link"><i class="sidenav-icon ion ion-md-bookmarks"></i><div>Reisenews</div></a>
|
||||
</li>
|
||||
@endif
|
||||
|
||||
@if(Auth::user()->isPermission('cms-aq'))
|
||||
<li class="sidenav-item{{ Request::is('cms/answer_question', 'cms/answer_question/*') ? ' active' : '' }}">
|
||||
<a href="{{ route('cms_answer_question') }}" class="sidenav-link"><i class="sidenav-icon ion ion-ios-help-buoy"></i><div>Fragen & Antworten</div></a>
|
||||
@if (Auth::user()->isPermission('cms-fb'))
|
||||
<li class="sidenav-item{{ Request::is('cms/feedback', 'cms/feedback/*') ? ' active' : '' }}">
|
||||
<a href="{{ route('cms_feedback') }}" class="sidenav-link"><i
|
||||
class="sidenav-icon ion ion-md-chatboxes"></i>
|
||||
<div>Feedback</div>
|
||||
</a>
|
||||
</li>
|
||||
@endif
|
||||
@if(Auth::user()->isPermission('cms-sb'))
|
||||
<li class="sidenav-item{{ Request::is('cms/sidebar') ? ' active' : '' }}">
|
||||
<a href="{{ route('cms_sidebar') }}" class="sidenav-link"><i class="sidenav-icon ion ion-md-list"></i><div>Sidebar</div></a>
|
||||
</li>
|
||||
@if (Auth::user()->isPermission('cms-nw'))
|
||||
<li class="sidenav-item{{ Request::is('cms/news', 'cms/news/*') ? ' active' : '' }}">
|
||||
<a href="{{ route('cms_news') }}" class="sidenav-link"><i
|
||||
class="sidenav-icon ion ion-md-bookmarks"></i>
|
||||
<div>Reisenews</div>
|
||||
</a>
|
||||
</li>
|
||||
@endif
|
||||
@if(Auth::user()->isPermission('cms-cn'))
|
||||
|
||||
@if (Auth::user()->isPermission('cms-aq'))
|
||||
<li
|
||||
class="sidenav-item{{ Request::is('cms/answer_question', 'cms/answer_question/*') ? ' active' : '' }}">
|
||||
<a href="{{ route('cms_answer_question') }}" class="sidenav-link"><i
|
||||
class="sidenav-icon ion ion-ios-help-buoy"></i>
|
||||
<div>Fragen & Antworten</div>
|
||||
</a>
|
||||
</li>
|
||||
@endif
|
||||
@if (Auth::user()->isPermission('cms-sb'))
|
||||
<li class="sidenav-item{{ Request::is('cms/sidebar') ? ' active' : '' }}">
|
||||
<a href="{{ route('cms_sidebar') }}" class="sidenav-link"><i
|
||||
class="sidenav-icon ion ion-md-list"></i>
|
||||
<div>Sidebar</div>
|
||||
</a>
|
||||
</li>
|
||||
@endif
|
||||
@if (Auth::user()->isPermission('cms-cn'))
|
||||
<li class="sidenav-item{{ Request::is('cms/content/*') ? ' open' : '' }}">
|
||||
<a href="javascript:void(0)" class="sidenav-link sidenav-toggle">
|
||||
<i class="sidenav-icon ion ion-md-map"></i>
|
||||
<div>Inhalte</div>
|
||||
</a>
|
||||
<ul class="sidenav-menu">
|
||||
@if(Auth::user()->isPermission('cms-cn-in'))
|
||||
<li class="sidenav-item{{ Request::is('cms/content/infos') ? ' active' : '' }}">
|
||||
<a href="{{ route('cms_content_infos') }}" class="sidenav-link"><i class="sidenav-icon ion ion-ios-business"></i><div>Infos</div></a>
|
||||
</li>
|
||||
@if (Auth::user()->isPermission('cms-cn-in'))
|
||||
<li class="sidenav-item{{ Request::is('cms/content/infos') ? ' active' : '' }}">
|
||||
<a href="{{ route('cms_content_infos') }}" class="sidenav-link"><i
|
||||
class="sidenav-icon ion ion-ios-business"></i>
|
||||
<div>Infos</div>
|
||||
</a>
|
||||
</li>
|
||||
@endif
|
||||
@if(Auth::user()->isPermission('cms-cn-al'))
|
||||
@if (Auth::user()->isPermission('cms-cn-al'))
|
||||
<li class="sidenav-item{{ Request::is('cms/content/all') ? ' active' : '' }}">
|
||||
<a href="{{ route('cms_content_all') }}" class="sidenav-link"><i class="sidenav-icon ion ion-ios-paper"></i><div>Allgemein</div></a>
|
||||
<a href="{{ route('cms_content_all') }}" class="sidenav-link"><i
|
||||
class="sidenav-icon ion ion-ios-paper"></i>
|
||||
<div>Allgemein</div>
|
||||
</a>
|
||||
</li>
|
||||
@endif
|
||||
@if(Auth::user()->isPermission('cms-cn-au'))
|
||||
@if (Auth::user()->isPermission('cms-cn-au'))
|
||||
<li class="sidenav-item{{ Request::is('cms/content/author') ? ' active' : '' }}">
|
||||
<a href="{{ route('cms_content_author') }}" class="sidenav-link"><i class="sidenav-icon ion ion-ios-finger-print"></i><div>Autoren</div></a>
|
||||
<a href="{{ route('cms_content_author') }}" class="sidenav-link"><i
|
||||
class="sidenav-icon ion ion-ios-finger-print"></i>
|
||||
<div>Autoren</div>
|
||||
</a>
|
||||
</li>
|
||||
@endif
|
||||
|
||||
</ul>
|
||||
</li>
|
||||
@endif
|
||||
@if (Auth::user()->isPermission('crm-nav-api'))
|
||||
<li class="sidenav-item{{ Request::is(['navigation-api']) ? ' active' : '' }}">
|
||||
<a href="{{ route('navigation_api') }}" class="sidenav-link"><i
|
||||
class="sidenav-icon ion ion-md-map"></i>
|
||||
<div>Navigation API</div>
|
||||
</a>
|
||||
</li>
|
||||
@endif
|
||||
@if (Auth::user()->isPermission('cms-newsletter'))
|
||||
<li class="sidenav-item{{ Request::is('newsletter', 'newsletter/*') ? ' active' : '' }}">
|
||||
<a href="{{ route('newsletter.index') }}" class="sidenav-link"><i
|
||||
class="sidenav-icon ion ion-md-mail"></i>
|
||||
<div>Newsletter</div>
|
||||
</a>
|
||||
</li>
|
||||
@endif
|
||||
@endif
|
||||
@endif
|
||||
@endif
|
||||
|
||||
@if(Auth::user()->isSuperAdmin())
|
||||
@if (Auth::user()->isSuperAdmin())
|
||||
<li class="sidenav-divider mb-1"></li>
|
||||
<li class="sidenav-header small font-weight-semibold">SUPERADMIN</li>
|
||||
@if(Auth::user()->isPermission('sua-st'))
|
||||
@if (Auth::user()->isPermission('sua-st'))
|
||||
<li class="sidenav-item{{ Request::is('admin/settings/*') ? ' open' : '' }}">
|
||||
<a href="javascript:void(0)" class="sidenav-link sidenav-toggle">
|
||||
<i class="sidenav-icon ion ion-ios-settings"></i>
|
||||
<div>Einstellungen</div>
|
||||
</a>
|
||||
<ul class="sidenav-menu">
|
||||
@if(Auth::user()->isPermission('sua-st-al'))
|
||||
@if (Auth::user()->isPermission('sua-st-al'))
|
||||
<li class="sidenav-item{{ Request::is('admin/settings/airline') ? ' active' : '' }}">
|
||||
<a href="{{ route('admin_settings_airline') }}" class="sidenav-link"><i class="sidenav-icon ion ion-md-airplane"></i><div>{{ __('Airline') }}</div></a>
|
||||
<a href="{{ route('admin_settings_airline') }}" class="sidenav-link"><i
|
||||
class="sidenav-icon ion ion-md-airplane"></i>
|
||||
<div>{{ __('Airline') }}</div>
|
||||
</a>
|
||||
</li>
|
||||
@endif
|
||||
@if(Auth::user()->isPermission('sua-st-ap'))
|
||||
<li class="sidenav-item{{ Request::is('admin/settings/airport') ? ' active' : '' }}">
|
||||
<a href="{{ route('admin_settings_airport') }}" class="sidenav-link"><i class="sidenav-icon ion ion-ios-airplane"></i><div>{{ __('Airport') }}</div></a>
|
||||
</li>
|
||||
@endif
|
||||
@if(Auth::user()->isPermission('sua-st-em'))
|
||||
@if (Auth::user()->isPermission('sua-st-ap'))
|
||||
<li class="sidenav-item{{ Request::is('admin/settings/airport') ? ' active' : '' }}">
|
||||
<a href="{{ route('admin_settings_airport') }}" class="sidenav-link"><i
|
||||
class="sidenav-icon ion ion-ios-airplane"></i>
|
||||
<div>{{ __('Airport') }}</div>
|
||||
</a>
|
||||
</li>
|
||||
@endif
|
||||
@if (Auth::user()->isPermission('sua-st-em'))
|
||||
<li class="sidenav-item{{ Request::is('admin/settings/emails') ? ' active' : '' }}">
|
||||
<a href="{{ route('admin_settings_emails') }}" class="sidenav-link"><i class="sidenav-icon ion ion-ios-mail"></i><div>{{ __('E-Mails') }}</div></a>
|
||||
<a href="{{ route('admin_settings_emails') }}" class="sidenav-link"><i
|
||||
class="sidenav-icon ion ion-ios-mail"></i>
|
||||
<div>{{ __('E-Mails') }}</div>
|
||||
</a>
|
||||
</li>
|
||||
@endif
|
||||
@if(Auth::user()->isPermission('sua-st-ke'))
|
||||
@if (Auth::user()->isPermission('sua-st-ke'))
|
||||
<li class="sidenav-item{{ Request::is('admin/settings/keyword') ? ' active' : '' }}">
|
||||
<a href="{{ route('admin_settings_keyword') }}" class="sidenav-link"><i class="sidenav-icon ion ion-ios-key"></i><div>{{ __('Keywords') }}</div></a>
|
||||
<a href="{{ route('admin_settings_keyword') }}" class="sidenav-link"><i
|
||||
class="sidenav-icon ion ion-ios-key"></i>
|
||||
<div>{{ __('Keywords') }}</div>
|
||||
</a>
|
||||
</li>
|
||||
@endif
|
||||
@if(Auth::user()->isPermission('sua-st-sp'))
|
||||
<li class="sidenav-item{{ Request::is('admin/settings/service_provider') ? ' active' : '' }}">
|
||||
<a href="{{ route('admin_settings_service_provider') }}" class="sidenav-link"><i class="sidenav-icon ion ion-ios-archive"></i><div>{{ __('Leistungsträger') }}</div></a>
|
||||
@if (Auth::user()->isPermission('sua-st-sp'))
|
||||
<li
|
||||
class="sidenav-item{{ Request::is('admin/settings/service_provider') ? ' active' : '' }}">
|
||||
<a href="{{ route('admin_settings_service_provider') }}" class="sidenav-link"><i
|
||||
class="sidenav-icon ion ion-ios-archive"></i>
|
||||
<div>{{ __('Leistungsträger') }}</div>
|
||||
</a>
|
||||
</li>
|
||||
@endif
|
||||
@if(Auth::user()->isPermission('sua-st-tn'))
|
||||
<li class="sidenav-item{{ Request::is('admin/settings/travel_nationality') ? ' active' : '' }}">
|
||||
<a href="{{ route('admin_settings_travel_nationality') }}" class="sidenav-link"><i class="sidenav-icon ion ion-ios-people"></i><div>{{ __('Nationalitäten') }}</div></a>
|
||||
@if (Auth::user()->isPermission('sua-st-tn'))
|
||||
<li
|
||||
class="sidenav-item{{ Request::is('admin/settings/travel_nationality') ? ' active' : '' }}">
|
||||
<a href="{{ route('admin_settings_travel_nationality') }}"
|
||||
class="sidenav-link"><i class="sidenav-icon ion ion-ios-people"></i>
|
||||
<div>{{ __('Nationalitäten') }}</div>
|
||||
</a>
|
||||
</li>
|
||||
@endif
|
||||
@if(Auth::user()->isPermission('sua-st-co'))
|
||||
<li class="sidenav-item{{ Request::is('admin/settings/travel_country') ? ' active' : '' }} {{ Request::is('admin/settings/travel_country/*') ? ' active' : '' }}">
|
||||
<a href="{{ route('admin_settings_travel_country') }}" class="sidenav-link"><i class="sidenav-icon ion ion-ios-globe"></i><div>{{ __('Reiseländer') }}</div></a>
|
||||
@if (Auth::user()->isPermission('sua-st-co'))
|
||||
<li
|
||||
class="sidenav-item{{ Request::is('admin/settings/travel_country') ? ' active' : '' }} {{ Request::is('admin/settings/travel_country/*') ? ' active' : '' }}">
|
||||
<a href="{{ route('admin_settings_travel_country') }}" class="sidenav-link"><i
|
||||
class="sidenav-icon ion ion-ios-globe"></i>
|
||||
<div>{{ __('Reiseländer') }}</div>
|
||||
</a>
|
||||
</li>
|
||||
@endif
|
||||
@if(Auth::user()->isPermission('sua-st-tpl'))
|
||||
<li class="sidenav-item{{ Request::is('admin/settings/travel_places') ? ' active' : '' }} {{ Request::is('admin/settings/travel_place/*') ? ' active' : '' }}">
|
||||
<a href="{{ route('admin_settings_travel_places') }}" class="sidenav-link"><i class="sidenav-icon ion ion-ios-pin"></i><div>{{ __('Reiseorte') }}</div></a>
|
||||
</li>
|
||||
@endif
|
||||
@if(Auth::user()->isPermission('sua-st-tp'))
|
||||
<li class="sidenav-item{{ Request::is('admin/settings/travel_program') ? ' active' : '' }}">
|
||||
<a href="{{ route('admin_settings_travel_program') }}" class="sidenav-link"><i class="sidenav-icon ion ion-ios-airplane"></i><div>{{ __('Reiseprogramme') }}</div></a>
|
||||
</li>
|
||||
@endif
|
||||
@if(Auth::user()->isPermission('sua-st-bs'))
|
||||
<li class="sidenav-item{{ Request::is('admin/settings/booking_status') ? ' active' : '' }}">
|
||||
<a href="{{ route('admin_settings_booking_status') }}" class="sidenav-link"><i class="sidenav-icon ion ion-ios-stats"></i><div>{{ __('Reisestatus') }}</div></a>
|
||||
</li>
|
||||
@endif
|
||||
@if(Auth::user()->isPermission('sua-st-tc'))
|
||||
<li class="sidenav-item{{ Request::is('admin/settings/travel_company') ? ' active' : '' }}">
|
||||
<a href="{{ route('admin_settings_travel_company') }}" class="sidenav-link"><i class="sidenav-icon ion ion-ios-archive"></i><div>{{ __('Veranstalter') }}</div></a>
|
||||
@if (Auth::user()->isPermission('sua-st-tpl'))
|
||||
<li
|
||||
class="sidenav-item{{ Request::is('admin/settings/travel_places') ? ' active' : '' }} {{ Request::is('admin/settings/travel_place/*') ? ' active' : '' }}">
|
||||
<a href="{{ route('admin_settings_travel_places') }}" class="sidenav-link"><i
|
||||
class="sidenav-icon ion ion-ios-pin"></i>
|
||||
<div>{{ __('Reiseorte') }}</div>
|
||||
</a>
|
||||
</li>
|
||||
@endif
|
||||
@if(Auth::user()->isPermission('sua-st-tca'))
|
||||
<li class="sidenav-item{{ Request::is('admin/settings/travel_category') ? ' active' : '' }}">
|
||||
<a href="{{ route('admin_settings_travel_category') }}" class="sidenav-link"><i class="sidenav-icon ion ion-ios-briefcase"></i><div>{{ __('Reiseart') }}</div></a>
|
||||
</li>
|
||||
@endif
|
||||
@if(Auth::user()->isPermission('sua-st-tap'))
|
||||
<li class="sidenav-item{{ Request::is('admin/settings/travel_arrival_point') ? ' active' : '' }}">
|
||||
<a href="{{ route('admin_settings_travel_arrival_point') }}" class="sidenav-link"><i class="sidenav-icon ion ion-ios-pin"></i><div>{{ __('Zielflughafen') }}</div></a>
|
||||
</li>
|
||||
@endif
|
||||
@if(Auth::user()->isPermission('sua-st-in'))
|
||||
<li class="sidenav-item{{ Request::is('admin/settings/insurance') ? ' active' : '' }}">
|
||||
<a href="{{ route('admin_settings_insurance') }}" class="sidenav-link"><i class="sidenav-icon ion ion-ios-umbrella"></i><div>{{ __('Versicherungen') }}</div></a>
|
||||
@if (Auth::user()->isPermission('sua-st-tp'))
|
||||
<li
|
||||
class="sidenav-item{{ Request::is('admin/settings/travel_program') ? ' active' : '' }}">
|
||||
<a href="{{ route('admin_settings_travel_program') }}" class="sidenav-link"><i
|
||||
class="sidenav-icon ion ion-ios-airplane"></i>
|
||||
<div>{{ __('Reiseprogramme') }}</div>
|
||||
</a>
|
||||
</li>
|
||||
@endif
|
||||
@if(Auth::user()->isPermission('sua-st-tgn'))
|
||||
<li class="sidenav-item{{ Request::is('admin/settings/gerneral_notes') ? ' active' : '' }}">
|
||||
<a href="{{ route('admin_settings_gerneral_notes') }}" class="sidenav-link"><i class="sidenav-icon ion ion-ios-paper"></i><div>{{ __('Reisehinweise') }}</div></a>
|
||||
</li>
|
||||
@if (Auth::user()->isPermission('sua-st-bs'))
|
||||
<li
|
||||
class="sidenav-item{{ Request::is('admin/settings/booking_status') ? ' active' : '' }}">
|
||||
<a href="{{ route('admin_settings_booking_status') }}" class="sidenav-link"><i
|
||||
class="sidenav-icon ion ion-ios-stats"></i>
|
||||
<div>{{ __('Reisestatus') }}</div>
|
||||
</a>
|
||||
</li>
|
||||
@endif
|
||||
@if (Auth::user()->isPermission('sua-st-tc'))
|
||||
<li
|
||||
class="sidenav-item{{ Request::is('admin/settings/travel_company') ? ' active' : '' }}">
|
||||
<a href="{{ route('admin_settings_travel_company') }}" class="sidenav-link"><i
|
||||
class="sidenav-icon ion ion-ios-archive"></i>
|
||||
<div>{{ __('Veranstalter') }}</div>
|
||||
</a>
|
||||
</li>
|
||||
@endif
|
||||
@if (Auth::user()->isPermission('sua-st-tca'))
|
||||
<li
|
||||
class="sidenav-item{{ Request::is('admin/settings/travel_category') ? ' active' : '' }}">
|
||||
<a href="{{ route('admin_settings_travel_category') }}" class="sidenav-link"><i
|
||||
class="sidenav-icon ion ion-ios-briefcase"></i>
|
||||
<div>{{ __('Reiseart') }}</div>
|
||||
</a>
|
||||
</li>
|
||||
@endif
|
||||
@if (Auth::user()->isPermission('sua-st-tap'))
|
||||
<li
|
||||
class="sidenav-item{{ Request::is('admin/settings/travel_arrival_point') ? ' active' : '' }}">
|
||||
<a href="{{ route('admin_settings_travel_arrival_point') }}"
|
||||
class="sidenav-link"><i class="sidenav-icon ion ion-ios-pin"></i>
|
||||
<div>{{ __('Zielflughafen') }}</div>
|
||||
</a>
|
||||
</li>
|
||||
@endif
|
||||
@if (Auth::user()->isPermission('sua-st-in'))
|
||||
<li
|
||||
class="sidenav-item{{ Request::is('admin/settings/insurance') ? ' active' : '' }}">
|
||||
<a href="{{ route('admin_settings_insurance') }}" class="sidenav-link"><i
|
||||
class="sidenav-icon ion ion-ios-umbrella"></i>
|
||||
<div>{{ __('Versicherungen') }}</div>
|
||||
</a>
|
||||
</li>
|
||||
@endif
|
||||
@if (Auth::user()->isPermission('sua-st-tgn'))
|
||||
<li
|
||||
class="sidenav-item{{ Request::is('admin/settings/gerneral_notes') ? ' active' : '' }}">
|
||||
<a href="{{ route('admin_settings_gerneral_notes') }}" class="sidenav-link"><i
|
||||
class="sidenav-icon ion ion-ios-paper"></i>
|
||||
<div>{{ __('Reisehinweise') }}</div>
|
||||
</a>
|
||||
</li>
|
||||
@endif
|
||||
|
||||
@if (Auth::user()->isPermission('sua-st-ca'))
|
||||
<li
|
||||
class="sidenav-item{{ Request::is('admin/settings/categories') ? ' active' : '' }}">
|
||||
<a href="{{ route('admin_settings_categories') }}" class="sidenav-link"><i
|
||||
class="sidenav-icon ion ion-ios-list-box"></i>
|
||||
<div>{{ __('Kategorien') }}</div>
|
||||
</a>
|
||||
</li>
|
||||
@endif
|
||||
|
||||
@if(Auth::user()->isPermission('sua-st-ca'))
|
||||
<li class="sidenav-item{{ Request::is('admin/settings/categories') ? ' active' : '' }}">
|
||||
<a href="{{ route('admin_settings_categories') }}" class="sidenav-link"><i class="sidenav-icon ion ion-ios-list-box"></i><div>{{ __('Kategorien') }}</div></a>
|
||||
</li>
|
||||
@endif
|
||||
</ul>
|
||||
</li>
|
||||
@endif
|
||||
@endif
|
||||
|
||||
|
||||
@if(Auth::user()->isPermission('sua-re'))
|
||||
@if (Auth::user()->isPermission('sua-re'))
|
||||
<li class="sidenav-item{{ Request::is('admin/report/*') ? ' open' : '' }}">
|
||||
<a href="javascript:void(0)" class="sidenav-link sidenav-toggle">
|
||||
<i class="sidenav-icon ion ion-md-analytics"></i>
|
||||
<div>Reporting</div>
|
||||
</a>
|
||||
<ul class="sidenav-menu">
|
||||
@if(Auth::user()->isPermission('sua-re-bo'))
|
||||
@if (Auth::user()->isPermission('sua-re-bo'))
|
||||
<li class="sidenav-item{{ Request::is('admin/report/bookings') ? ' active' : '' }}">
|
||||
<a href="{{ route('admin_report_bookings') }}" class="sidenav-link"><i class="sidenav-icon ion ion-md-bed"></i><div>{{ __('Buchungen & Leistungen') }}</div></a>
|
||||
<a href="{{ route('admin_report_bookings') }}" class="sidenav-link"><i
|
||||
class="sidenav-icon ion ion-md-bed"></i>
|
||||
<div>{{ __('Buchungen & Leistungen') }}</div>
|
||||
</a>
|
||||
</li>
|
||||
|
||||
@endif
|
||||
@if(Auth::user()->isPermission('sua-re-pp'))
|
||||
@if (Auth::user()->isPermission('sua-re-pp'))
|
||||
<li class="sidenav-item{{ Request::is('admin/report/providers') ? ' active' : '' }}">
|
||||
<a href="{{ route('admin_report_providers') }}" class="sidenav-link"><i class="sidenav-icon ion ion-md-cash"></i><div>{{ __('Leistungsträger-Zahlungen') }}</div></a>
|
||||
<a href="{{ route('admin_report_providers') }}" class="sidenav-link"><i
|
||||
class="sidenav-icon ion ion-md-cash"></i>
|
||||
<div>{{ __('Leistungsträger-Zahlungen') }}</div>
|
||||
</a>
|
||||
</li>
|
||||
@endif
|
||||
@if(Auth::user()->isPermission('sua-re-bo'))
|
||||
<li class="sidenav-item{{ Request::is('admin/report/check/bookings') ? ' active' : '' }}">
|
||||
<a href="{{ route('admin_report_check_bookings') }}" class="sidenav-link"><i class="sidenav-icon ion ion-md-flash"></i><div>{{ __('Preise pürfen') }}</div></a>
|
||||
</li>
|
||||
@if (Auth::user()->isPermission('sua-re-bo'))
|
||||
<li
|
||||
class="sidenav-item{{ Request::is('admin/report/check/bookings') ? ' active' : '' }}">
|
||||
<a href="{{ route('admin_report_check_bookings') }}" class="sidenav-link"><i
|
||||
class="sidenav-icon ion ion-md-flash"></i>
|
||||
<div>{{ __('Preise pürfen') }}</div>
|
||||
</a>
|
||||
</li>
|
||||
@endif
|
||||
@if(Auth::user()->isPermission('sua-re-fw'))
|
||||
<li class="sidenav-item{{ Request::is('admin/report/fewo') ? ' active' : '' }}">
|
||||
<a href="{{ route('admin_report_fewo') }}" class="sidenav-link"><i class="sidenav-icon ion ion-ios-home"></i><div>{{ __('Buchungen FeWo') }}</div></a>
|
||||
</li>
|
||||
@if (Auth::user()->isPermission('sua-re-fw'))
|
||||
<li class="sidenav-item{{ Request::is('admin/report/fewo') ? ' active' : '' }}">
|
||||
<a href="{{ route('admin_report_fewo') }}" class="sidenav-link"><i
|
||||
class="sidenav-icon ion ion-ios-home"></i>
|
||||
<div>{{ __('Buchungen FeWo') }}</div>
|
||||
</a>
|
||||
</li>
|
||||
@endif
|
||||
|
||||
@if(Auth::user()->isPermission('sua-re-le'))
|
||||
<li class="sidenav-item{{ Request::is('admin/report/leads') ? ' active' : '' }}">
|
||||
<a href="{{ route('admin_report_leads') }}" class="sidenav-link"><i class="sidenav-icon ion ion-ios-contact"></i><div>{{ __('Buchungen Anfragen') }}</div></a>
|
||||
</li>
|
||||
@if (Auth::user()->isPermission('sua-re-le'))
|
||||
<li class="sidenav-item{{ Request::is('admin/report/leads') ? ' active' : '' }}">
|
||||
<a href="{{ route('admin_report_leads') }}" class="sidenav-link"><i
|
||||
class="sidenav-icon ion ion-ios-contact"></i>
|
||||
<div>{{ __('Buchungen Anfragen') }}</div>
|
||||
</a>
|
||||
</li>
|
||||
@endif
|
||||
|
||||
</ul>
|
||||
</li>
|
||||
@endif
|
||||
@if(Auth::user()->isPermission('sua-ur-rt'))
|
||||
<li class="sidenav-item{{ Request::is('admin/users') ? ' active' : '' }} {{ Request::is('admin/user/edit/*') ? ' active' : '' }}">
|
||||
<a href="{{ route('admin_users') }}" class="sidenav-link"><i class="sidenav-icon ion ion-ios-ribbon"></i><div>{{ __('User Rechte') }}</div></a>
|
||||
</li>
|
||||
@endif
|
||||
@if (Auth::user()->isPermission('sua-ur-rt'))
|
||||
<li
|
||||
class="sidenav-item{{ Request::is('admin/users') ? ' active' : '' }} {{ Request::is('admin/user/edit/*') ? ' active' : '' }}">
|
||||
<a href="{{ route('admin_users') }}" class="sidenav-link"><i
|
||||
class="sidenav-icon ion ion-ios-ribbon"></i>
|
||||
<div>{{ __('User Rechte') }}</div>
|
||||
</a>
|
||||
</li>
|
||||
@endif
|
||||
@endif
|
||||
|
||||
@if(Auth::user()->isSySAdmin())
|
||||
@if (Auth::user()->isSySAdmin())
|
||||
<li class="sidenav-divider mb-1"></li>
|
||||
<li class="sidenav-header small font-weight-semibold">SYSADMIN</li>
|
||||
<li class="sidenav-item{{ Request::is('sysadmin/tools', 'sysadmin/tool/*') ? ' active' : '' }}">
|
||||
<a href="{{ route('sysadmin_tools') }}" class="sidenav-link"><i class="sidenav-icon ion ion-ios-cog"></i><div>Tools</div></a>
|
||||
<a href="{{ route('sysadmin_tools') }}" class="sidenav-link"><i
|
||||
class="sidenav-icon ion ion-ios-cog"></i>
|
||||
<div>Tools</div>
|
||||
</a>
|
||||
</li>
|
||||
@endif
|
||||
|
||||
<li class="sidenav-divider mb-1"></li>
|
||||
<li class="sidenav-divider mb-1"></li>
|
||||
|
||||
<li class="sidenav-item">
|
||||
<a href="{{ route('logout') }}" class="sidenav-link"><i class="sidenav-icon ion ion-ios-power"></i><div>{{ __('Logout') }}</div></a>
|
||||
</li>
|
||||
<li class="sidenav-item">
|
||||
<a href="{{ route('logout') }}" class="sidenav-link"><i class="sidenav-icon ion ion-ios-power"></i>
|
||||
<div>{{ __('Logout') }}</div>
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
|
|
|
|||
560
resources/views/navigation/index.blade.php
Normal file
|
|
@ -0,0 +1,560 @@
|
|||
@extends('layouts.layout-2')
|
||||
|
||||
@section('content')
|
||||
<h4 class="font-weight-bold py-3 mb-1">
|
||||
<i class="ion ion-md-map"></i> Navigation API - Navigationsbaum (Frontend-Struktur)
|
||||
</h4>
|
||||
<div class="alert alert-info">
|
||||
<i class="fa fa-info-circle"></i> Dieser Baum zeigt die Navigation genau so, wie sie im Frontend (header.html.twig)
|
||||
angezeigt wird.
|
||||
<br>
|
||||
<strong>Bereiche:</strong>
|
||||
<ul class="mb-0 mt-2">
|
||||
<li><strong>Länder-Navigation:</strong> Länderseiten mit Children, gruppiert nach "Haupt" und "Infos"</li>
|
||||
<li><strong>USEDOM Ferienwohnungen:</strong> Ferienwohnungs-Übersicht mit einzelnen FeWos</li>
|
||||
<li><strong>Weitere Seiten:</strong> Seiten aus dem "Mehr"-Menü (Über uns, Reiseversicherung, Reiseführer, etc.)
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<!-- Statistiken -->
|
||||
<div class="row mb-4">
|
||||
<div class="col-md-3">
|
||||
<div class="card">
|
||||
<div class="card-body text-center">
|
||||
<div class="display-4 text-primary" id="stat-total">-</div>
|
||||
<small class="text-muted">Gesamt Seiten</small>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<div class="card">
|
||||
<div class="card-body text-center">
|
||||
<div class="display-4 text-success" id="stat-active">-</div>
|
||||
<small class="text-muted">Aktive Seiten</small>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<div class="card">
|
||||
<div class="card-body text-center">
|
||||
<div class="display-4 text-warning" id="stat-programs">-</div>
|
||||
<small class="text-muted">Reiseprogramme</small>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<div class="card">
|
||||
<div class="card-body text-center">
|
||||
<div class="display-4 text-info" id="stat-countries">-</div>
|
||||
<small class="text-muted">Länderseiten</small>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Toolbar -->
|
||||
<div class="card mb-3">
|
||||
<div class="card-body">
|
||||
<div class="row align-items-center">
|
||||
<div class="col-md-4">
|
||||
<div class="input-group">
|
||||
<input type="text" id="search-input" class="form-control"
|
||||
placeholder="Suche nach Titel, Slug oder URL...">
|
||||
<div class="input-group-append">
|
||||
<button class="btn btn-primary" id="search-btn"><i class="fa fa-search"></i></button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-8 text-right">
|
||||
<div class="btn-group" role="group">
|
||||
<button class="btn btn-secondary" id="expand-all-btn"><i class="fa fa-plus-square"></i> Alle
|
||||
aufklappen</button>
|
||||
<button class="btn btn-secondary" id="collapse-all-btn"><i class="fa fa-minus-square"></i> Alle
|
||||
zuklappen</button>
|
||||
</div>
|
||||
<div class="btn-group ml-2" role="group">
|
||||
<button class="btn btn-info" id="filter-hidden-btn" data-filter="with_hidden"><i
|
||||
class="fa fa-eye"></i>
|
||||
Mit ausgeblendeten</button>
|
||||
</div>
|
||||
<div class="btn-group ml-2" role="group">
|
||||
<button class="btn btn-success" id="export-btn"><i class="fa fa-download"></i> Export JSON</button>
|
||||
<button class="btn btn-warning" id="clear-cache-btn"><i class="fa fa-sync"></i> Cache
|
||||
leeren</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Navigationsbaum -->
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h5 class="mb-0">
|
||||
<i class="fa fa-sitemap"></i> Navigationsbaum
|
||||
<span class="badge badge-primary ml-2" id="node-count">0 Knoten</span>
|
||||
</h5>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div id="loading" class="text-center py-5">
|
||||
<div class="spinner-border text-primary" role="status">
|
||||
<span class="sr-only">Lade...</span>
|
||||
</div>
|
||||
<p class="mt-3">Lade Navigationsbaum...</p>
|
||||
</div>
|
||||
<div id="tree-container" style="display: none;">
|
||||
<div id="navigation-tree"></div>
|
||||
</div>
|
||||
<div id="no-results" style="display: none;" class="text-center py-5 text-muted">
|
||||
<i class="fa fa-search fa-3x mb-3"></i>
|
||||
<p>Keine Ergebnisse gefunden.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Styles -->
|
||||
<style>
|
||||
.tree-node {
|
||||
margin-left: 0;
|
||||
padding: 5px 0;
|
||||
}
|
||||
|
||||
.tree-node-content {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 8px 12px;
|
||||
border-radius: 4px;
|
||||
transition: background-color 0.2s;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.tree-node-content:hover {
|
||||
background-color: #f5f5f5;
|
||||
}
|
||||
|
||||
.tree-toggle {
|
||||
width: 20px;
|
||||
min-width: 20px;
|
||||
height: 20px;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
cursor: pointer;
|
||||
margin-right: 8px;
|
||||
color: #666;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.tree-toggle:hover {
|
||||
color: #000;
|
||||
}
|
||||
|
||||
.tree-toggle.empty {
|
||||
visibility: hidden;
|
||||
}
|
||||
|
||||
.tree-icon {
|
||||
width: 24px;
|
||||
min-width: 24px;
|
||||
margin-right: 8px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.tree-title {
|
||||
flex: 1;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.tree-badges {
|
||||
display: flex;
|
||||
gap: 5px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.tree-url {
|
||||
font-size: 12px;
|
||||
color: #999;
|
||||
margin-left: 8px;
|
||||
}
|
||||
|
||||
.tree-children {
|
||||
margin-left: 30px;
|
||||
border-left: 2px solid #e0e0e0;
|
||||
padding-left: 10px;
|
||||
}
|
||||
|
||||
.tree-children.collapsed {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.tree-node.inactive .tree-node-content {
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
.tree-node.filtered {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.tree-node.highlight .tree-node-content {
|
||||
background-color: #fff3cd;
|
||||
border-left: 3px solid #ffc107;
|
||||
}
|
||||
|
||||
.badge-small {
|
||||
font-size: 10px;
|
||||
padding: 2px 6px;
|
||||
}
|
||||
|
||||
/* Separator-Styles */
|
||||
.tree-separator {
|
||||
margin: 15px 0 10px 30px;
|
||||
padding: 8px 12px;
|
||||
background: linear-gradient(to right, #f8f9fa 0%, #e9ecef 100%);
|
||||
border-left: 4px solid #6c757d;
|
||||
border-radius: 4px;
|
||||
font-weight: bold;
|
||||
color: #495057;
|
||||
}
|
||||
|
||||
.tree-separator i {
|
||||
margin-right: 8px;
|
||||
color: #6c757d;
|
||||
}
|
||||
|
||||
/* Section-Separator (größere Trennung) */
|
||||
.tree-section-separator {
|
||||
margin: 30px 0 20px 0;
|
||||
padding: 12px 20px;
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
border-radius: 6px;
|
||||
font-weight: bold;
|
||||
font-size: 18px;
|
||||
color: white;
|
||||
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.tree-section-separator i {
|
||||
margin-right: 10px;
|
||||
}
|
||||
|
||||
/* Frontend-spezifische Styles */
|
||||
.tree-node.country-page>.tree-node-content {
|
||||
background-color: #f8f9fa;
|
||||
font-weight: 600;
|
||||
font-size: 16px;
|
||||
border-left: 4px solid #007bff;
|
||||
padding-left: 16px;
|
||||
}
|
||||
|
||||
.tree-node.country-page>.tree-node-content:hover {
|
||||
background-color: #e9ecef;
|
||||
}
|
||||
|
||||
.tree-node.info-page>.tree-node-content {
|
||||
background-color: #fff9e6;
|
||||
}
|
||||
</style>
|
||||
|
||||
<!-- Scripts -->
|
||||
<script>
|
||||
let navigationData = null;
|
||||
let currentFilter = 'with_hidden';
|
||||
let searchQuery = '';
|
||||
|
||||
// Lade Statistiken
|
||||
function loadStats() {
|
||||
fetch('/navigation-api/stats')
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
if (data.success) {
|
||||
$('#stat-total').text(data.data.total_pages);
|
||||
$('#stat-active').text(data.data.active_nodes);
|
||||
$('#stat-programs').text(data.data.travel_programs);
|
||||
$('#stat-countries').text(data.data.country_pages);
|
||||
}
|
||||
})
|
||||
.catch(error => console.error('Error loading stats:', error));
|
||||
}
|
||||
|
||||
// Lade Navigationsbaum
|
||||
function loadNavigationTree(includeHidden = true) {
|
||||
$('#loading').show();
|
||||
$('#tree-container').hide();
|
||||
$('#no-results').hide();
|
||||
|
||||
const url = `/navigation-api/data?include_hidden=${includeHidden ? '1' : '0'}`;
|
||||
|
||||
fetch(url)
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
if (data.success) {
|
||||
navigationData = data.data;
|
||||
renderTree(navigationData);
|
||||
updateNodeCount(data.meta.total_nodes);
|
||||
$('#loading').hide();
|
||||
$('#tree-container').show();
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('Error loading tree:', error);
|
||||
$('#loading').hide();
|
||||
alert('Fehler beim Laden des Navigationsbaums');
|
||||
});
|
||||
}
|
||||
|
||||
// Rendere Baum
|
||||
function renderTree(nodes) {
|
||||
const container = $('#navigation-tree');
|
||||
container.empty();
|
||||
|
||||
if (nodes.length === 0) {
|
||||
$('#tree-container').hide();
|
||||
$('#no-results').show();
|
||||
return;
|
||||
}
|
||||
|
||||
// Rendere Nodes - unterscheide zwischen Separators und Pages
|
||||
nodes.forEach(node => {
|
||||
// Section-Separators und normale Separators haben keine Children-Logik
|
||||
if (node.is_section_separator || node.is_separator) {
|
||||
container.append(renderNode(node, false));
|
||||
} else {
|
||||
// Länderseiten (section = "Länder-Navigation") haben Children mit Toggle
|
||||
const isCountryPage = node.section === 'Länder-Navigation';
|
||||
container.append(renderNode(node, isCountryPage));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Rendere einzelnen Knoten
|
||||
function renderNode(node, isCountryPage = false) {
|
||||
// Prüfe ob es ein Section-Separator ist
|
||||
if (node.is_section_separator) {
|
||||
return $('<div>')
|
||||
.addClass('tree-section-separator')
|
||||
.html(`<i class="${node.icon}"></i>${node.title}`);
|
||||
}
|
||||
|
||||
// Prüfe ob es ein Separator ist
|
||||
if (node.is_separator) {
|
||||
return $('<div>')
|
||||
.addClass('tree-separator')
|
||||
.html(`<i class="${node.icon}"></i>${node.title}`);
|
||||
}
|
||||
|
||||
const hasChildren = node.has_children;
|
||||
const isActive = node.status === 1;
|
||||
const showInNavi = node.show_in_navi === 1;
|
||||
|
||||
const nodeDiv = $('<div>')
|
||||
.addClass('tree-node')
|
||||
.addClass(isActive ? 'active' : 'inactive')
|
||||
.addClass(isCountryPage ? 'country-page' : '')
|
||||
.addClass(node.before_title === 'Infos' ? 'info-page' : '')
|
||||
.data('node', node);
|
||||
|
||||
// Node Content
|
||||
const contentDiv = $('<div>')
|
||||
.addClass('tree-node-content');
|
||||
|
||||
// Toggle Button (nur für Country-Pages mit Children)
|
||||
const toggle = $('<span>')
|
||||
.addClass('tree-toggle')
|
||||
.addClass(hasChildren && isCountryPage ? '' : 'empty')
|
||||
.html(hasChildren && isCountryPage ? '<i class="fa fa-chevron-down"></i>' : '')
|
||||
.on('click', function(e) {
|
||||
e.stopPropagation();
|
||||
toggleNode(nodeDiv);
|
||||
});
|
||||
|
||||
// Icon
|
||||
const icon = $('<span>').addClass('tree-icon');
|
||||
if (node.is_country_page) {
|
||||
icon.html('<i class="fa fa-star text-warning"></i>');
|
||||
} else if (node.is_travel_program) {
|
||||
icon.html('<i class="fa fa-plane text-primary"></i>');
|
||||
} else if (node.is_fewo_lodging) {
|
||||
icon.html('<i class="fa fa-home text-info"></i>');
|
||||
} else if (node.before_title === 'Infos') {
|
||||
icon.html('<i class="fa fa-info-circle text-muted"></i>');
|
||||
} else {
|
||||
icon.html('<i class="fa fa-file-alt text-muted"></i>');
|
||||
}
|
||||
|
||||
// Title & URL
|
||||
const titleDiv = $('<div>').addClass('tree-title');
|
||||
|
||||
// Verwende titleShort wenn vorhanden, sonst title
|
||||
const displayTitle = node.title_short || node.title;
|
||||
titleDiv.append($('<span>').text(displayTitle));
|
||||
|
||||
if (node.url) {
|
||||
titleDiv.append($('<span>').addClass('tree-url').text(node.url));
|
||||
}
|
||||
|
||||
// Badges
|
||||
const badges = $('<div>').addClass('tree-badges');
|
||||
|
||||
if (!isActive) {
|
||||
badges.append($('<span>').addClass('badge badge-danger badge-small').text('Inaktiv'));
|
||||
}
|
||||
|
||||
if (!showInNavi) {
|
||||
badges.append($('<span>').addClass('badge badge-warning badge-small').text('Ausgeblendet'));
|
||||
}
|
||||
|
||||
if (node.is_travel_program) {
|
||||
badges.append($('<span>').addClass('badge badge-primary badge-small').text('Reiseprogramm'));
|
||||
}
|
||||
|
||||
if (node.is_fewo_lodging) {
|
||||
badges.append($('<span>').addClass('badge badge-info badge-small').text('FeWo'));
|
||||
}
|
||||
|
||||
if (node.is_country_page) {
|
||||
badges.append($('<span>').addClass('badge badge-success badge-small').text('Land'));
|
||||
}
|
||||
|
||||
if (node.before_title) {
|
||||
badges.append($('<span>').addClass('badge badge-secondary badge-small').text('Gruppe: ' + node
|
||||
.before_title));
|
||||
}
|
||||
|
||||
contentDiv.append(toggle, icon, titleDiv, badges);
|
||||
nodeDiv.append(contentDiv);
|
||||
|
||||
// Children (nur für Country-Pages)
|
||||
if (hasChildren && node.children && node.children.length > 0 && isCountryPage) {
|
||||
const childrenDiv = $('<div>').addClass('tree-children');
|
||||
node.children.forEach(child => {
|
||||
childrenDiv.append(renderNode(child, false));
|
||||
});
|
||||
nodeDiv.append(childrenDiv);
|
||||
}
|
||||
|
||||
return nodeDiv;
|
||||
}
|
||||
|
||||
// Toggle Node
|
||||
function toggleNode(nodeDiv) {
|
||||
const childrenDiv = nodeDiv.find('> .tree-children');
|
||||
const toggle = nodeDiv.find('> .tree-node-content > .tree-toggle i');
|
||||
|
||||
if (childrenDiv.hasClass('collapsed')) {
|
||||
childrenDiv.removeClass('collapsed');
|
||||
toggle.removeClass('fa-chevron-right').addClass('fa-chevron-down');
|
||||
} else {
|
||||
childrenDiv.addClass('collapsed');
|
||||
toggle.removeClass('fa-chevron-down').addClass('fa-chevron-right');
|
||||
}
|
||||
}
|
||||
|
||||
// Alle auf/zuklappen
|
||||
function expandAll() {
|
||||
$('.tree-children').removeClass('collapsed');
|
||||
$('.tree-toggle i').removeClass('fa-chevron-right').addClass('fa-chevron-down');
|
||||
}
|
||||
|
||||
function collapseAll() {
|
||||
$('.tree-children').addClass('collapsed');
|
||||
$('.tree-toggle i').removeClass('fa-chevron-down').addClass('fa-chevron-right');
|
||||
}
|
||||
|
||||
// Suche
|
||||
function performSearch(query) {
|
||||
searchQuery = query.toLowerCase();
|
||||
|
||||
if (!query) {
|
||||
$('.tree-node').removeClass('highlight filtered');
|
||||
return;
|
||||
}
|
||||
|
||||
$('.tree-node').each(function() {
|
||||
const node = $(this).data('node');
|
||||
const matches =
|
||||
node.title.toLowerCase().includes(searchQuery) ||
|
||||
(node.slug && node.slug.toLowerCase().includes(searchQuery)) ||
|
||||
(node.url && node.url.toLowerCase().includes(searchQuery));
|
||||
|
||||
if (matches) {
|
||||
$(this).removeClass('filtered').addClass('highlight');
|
||||
// Zeige Parent-Nodes
|
||||
$(this).parents('.tree-children').removeClass('collapsed');
|
||||
$(this).parents('.tree-node').find('> .tree-node-content > .tree-toggle i')
|
||||
.removeClass('fa-chevron-right').addClass('fa-chevron-down');
|
||||
} else {
|
||||
$(this).removeClass('highlight');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Node Count Update
|
||||
function updateNodeCount(count) {
|
||||
$('#node-count').text(`${count} Knoten`);
|
||||
}
|
||||
|
||||
// Export
|
||||
function exportJSON() {
|
||||
window.location.href = '/navigation-api/export?include_hidden=' + (currentFilter === 'with_hidden' ? '1' : '0');
|
||||
}
|
||||
|
||||
// Cache leeren
|
||||
function clearCache() {
|
||||
if (!confirm('Möchten Sie wirklich den Navigation-Cache leeren?')) {
|
||||
return;
|
||||
}
|
||||
|
||||
fetch('/navigation-api/clear-cache', {
|
||||
method: 'POST'
|
||||
})
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
if (data.success) {
|
||||
alert('Cache erfolgreich gelöscht');
|
||||
loadNavigationTree(currentFilter === 'active');
|
||||
} else {
|
||||
alert('Fehler beim Löschen des Cache');
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('Error clearing cache:', error);
|
||||
alert('Fehler beim Löschen des Cache');
|
||||
});
|
||||
}
|
||||
|
||||
// Event Listeners
|
||||
$(document).ready(function() {
|
||||
loadStats();
|
||||
loadNavigationTree();
|
||||
|
||||
$('#expand-all-btn').on('click', expandAll);
|
||||
$('#collapse-all-btn').on('click', collapseAll);
|
||||
$('#export-btn').on('click', exportJSON);
|
||||
$('#clear-cache-btn').on('click', clearCache);
|
||||
|
||||
$('#search-btn').on('click', function() {
|
||||
performSearch($('#search-input').val());
|
||||
});
|
||||
|
||||
$('#search-input').on('keyup', function(e) {
|
||||
if (e.key === 'Enter') {
|
||||
performSearch($(this).val());
|
||||
}
|
||||
});
|
||||
|
||||
$('#filter-hidden-btn').on('click', function() {
|
||||
if (currentFilter === 'with_hidden') {
|
||||
currentFilter = 'visible_only';
|
||||
$(this).html('<i class="fa fa-eye-slash"></i> Nur sichtbare').removeClass('btn-info')
|
||||
.addClass('btn-primary');
|
||||
} else {
|
||||
currentFilter = 'with_hidden';
|
||||
$(this).html('<i class="fa fa-eye"></i> Mit ausgeblendeten').removeClass('btn-primary')
|
||||
.addClass('btn-info');
|
||||
}
|
||||
loadNavigationTree(currentFilter === 'with_hidden');
|
||||
});
|
||||
});
|
||||
</script>
|
||||
@endsection
|
||||
235
resources/views/newsletter/detail.blade.php
Normal file
|
|
@ -0,0 +1,235 @@
|
|||
@extends('layouts.layout-2')
|
||||
|
||||
@section('content')
|
||||
<h4 class="d-flex justify-content-between align-items-center w-100 font-weight-bold py-3 mb-4">
|
||||
<div>
|
||||
<i class="ion ion-md-contact text-primary"></i> Newsletter-Kontakt
|
||||
</div>
|
||||
<div>
|
||||
<a href="{{ route('newsletter.index') }}" class="btn btn-secondary btn-sm">
|
||||
<i class="fa fa-arrow-left"></i> Zurück
|
||||
</a>
|
||||
<a href="{{ route('newsletter.edit', $contact->id) }}" class="btn btn-primary btn-sm">
|
||||
<i class="fa fa-edit"></i> Bearbeiten
|
||||
</a>
|
||||
</div>
|
||||
</h4>
|
||||
|
||||
@if (Session::has('alert-success'))
|
||||
<div class="alert alert-success alert-dismissible fade show">
|
||||
<button type="button" class="close" data-dismiss="alert">×</button>
|
||||
{{ Session::get('alert-success') }}
|
||||
</div>
|
||||
@endif
|
||||
|
||||
<div class="row">
|
||||
<!-- Linke Spalte -->
|
||||
<div class="col-md-8">
|
||||
<!-- Kontakt-Informationen -->
|
||||
<div class="card mb-4">
|
||||
<h6 class="card-header">Kontakt-Informationen</h6>
|
||||
<div class="card-body">
|
||||
<table class="table table-borderless">
|
||||
<tr>
|
||||
<th width="30%">E-Mail:</th>
|
||||
<td><a href="mailto:{{ $contact->email }}">{{ $contact->email }}</a></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>Name:</th>
|
||||
<td>{{ $contact->full_name ?: '-' }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>Vorname:</th>
|
||||
<td>{{ $contact->firstname ?: '-' }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>Nachname:</th>
|
||||
<td>{{ $contact->lastname ?: '-' }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>Status:</th>
|
||||
<td>{!! $contact->status_badge !!}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>Gruppen:</th>
|
||||
<td>
|
||||
@if ($contact->group_kulturreisen)
|
||||
<span class="badge badge-info">Kulturreisen</span>
|
||||
@endif
|
||||
@if ($contact->group_ferienwohnungen)
|
||||
<span class="badge badge-primary">Ferienwohnungen</span>
|
||||
@endif
|
||||
@if (!$contact->group_kulturreisen && !$contact->group_ferienwohnungen)
|
||||
<span class="text-muted">Keine Gruppe zugewiesen</span>
|
||||
@endif
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>Herkunft:</th>
|
||||
<td>{{ $contact->source_label }}</td>
|
||||
</tr>
|
||||
@if ($contact->notes)
|
||||
<tr>
|
||||
<th>Notizen:</th>
|
||||
<td>{{ $contact->notes }}</td>
|
||||
</tr>
|
||||
@endif
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Buchungsstatistiken -->
|
||||
<div class="card mb-4">
|
||||
<h6 class="card-header">Buchungsstatistiken</h6>
|
||||
<div class="card-body">
|
||||
<table class="table table-borderless">
|
||||
<tr>
|
||||
<th width="30%">Kulturreisen Buchungen:</th>
|
||||
<td>
|
||||
<span class="badge badge-secondary">{{ $contact->total_bookings_kulturreisen }}</span>
|
||||
@if ($contact->customer)
|
||||
<a href="{{ route('customer_detail', $contact->customer_id) }}"
|
||||
class="btn btn-sm btn-link">
|
||||
<i class="fa fa-external-link-alt"></i> Kunde anzeigen
|
||||
</a>
|
||||
@endif
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>Ferienwohnungen Buchungen:</th>
|
||||
<td>
|
||||
<span class="badge badge-secondary">{{ $contact->total_bookings_ferienwohnungen }}</span>
|
||||
@if ($contact->travel_user)
|
||||
<a href="{{ route('travel_user_detail', $contact->travel_user_id) }}"
|
||||
class="btn btn-sm btn-link">
|
||||
<i class="fa fa-external-link-alt"></i> Kunde anzeigen
|
||||
</a>
|
||||
@endif
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>Gesamt Buchungen:</th>
|
||||
<td><span class="badge badge-primary">{{ $contact->total_bookings }}</span></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>Letzte Buchung:</th>
|
||||
<td>{{ $contact->last_booking_at ? $contact->last_booking_at->format('d.m.Y') : '-' }}</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Aktivitäts-Log -->
|
||||
<div class="card">
|
||||
<h6 class="card-header">Aktivitäts-Log</h6>
|
||||
<div class="card-body">
|
||||
@if ($contact->logs->count() > 0)
|
||||
<div class="timeline">
|
||||
@foreach ($contact->logs as $log)
|
||||
<div class="timeline-item mb-3">
|
||||
<div class="d-flex">
|
||||
<div class="mr-3">
|
||||
<i
|
||||
class="fa fa-circle text-{{ $log->action === 'subscribed' ? 'success' : ($log->action === 'unsubscribed' ? 'danger' : 'info') }}"></i>
|
||||
</div>
|
||||
<div class="flex-grow-1">
|
||||
<div class="d-flex justify-content-between">
|
||||
<strong>{{ $log->action_label }}</strong>
|
||||
<small
|
||||
class="text-muted">{{ $log->created_at->format('d.m.Y H:i') }}</small>
|
||||
</div>
|
||||
@if ($log->description)
|
||||
<div class="text-muted">{{ $log->description }}</div>
|
||||
@endif
|
||||
@if ($log->user)
|
||||
<small class="text-muted">Durch: {{ $log->user->fullname }}</small>
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@endforeach
|
||||
</div>
|
||||
@else
|
||||
<p class="text-muted">Keine Aktivitäten vorhanden</p>
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Rechte Spalte -->
|
||||
<div class="col-md-4">
|
||||
<!-- Aktionen -->
|
||||
<div class="card mb-4">
|
||||
<h6 class="card-header">Aktionen</h6>
|
||||
<div class="card-body">
|
||||
@if ($contact->status === 'active')
|
||||
<form method="POST" action="{{ route('newsletter.unsubscribe', $contact->id) }}" class="mb-2">
|
||||
@csrf
|
||||
<div class="form-group">
|
||||
<label>Abmelde-Grund (optional)</label>
|
||||
<input type="text" name="reason" class="form-control form-control-sm"
|
||||
placeholder="z.B. Auf Wunsch des Kunden">
|
||||
</div>
|
||||
<button type="submit" class="btn btn-warning btn-block btn-sm"
|
||||
onclick="return confirm('Kontakt wirklich abmelden?')">
|
||||
<i class="fa fa-times"></i> Kontakt abmelden
|
||||
</button>
|
||||
</form>
|
||||
@elseif($contact->status === 'unsubscribed')
|
||||
<form method="POST" action="{{ route('newsletter.resubscribe', $contact->id) }}" class="mb-2">
|
||||
@csrf
|
||||
<button type="submit" class="btn btn-success btn-block btn-sm"
|
||||
onclick="return confirm('Kontakt wieder aktivieren?')">
|
||||
<i class="fa fa-check"></i> Wieder aktivieren
|
||||
</button>
|
||||
</form>
|
||||
@endif
|
||||
|
||||
<form method="POST" action="{{ route('newsletter.delete', $contact->id) }}" class="mb-2">
|
||||
@csrf
|
||||
@method('DELETE')
|
||||
<button type="submit" class="btn btn-danger btn-block btn-sm"
|
||||
onclick="return confirm('Kontakt wirklich löschen?')">
|
||||
<i class="fa fa-trash"></i> Kontakt löschen
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Zeitstempel -->
|
||||
<div class="card">
|
||||
<h6 class="card-header">Zeitstempel</h6>
|
||||
<div class="card-body">
|
||||
<table class="table table-borderless table-sm">
|
||||
<tr>
|
||||
<th>Erstellt:</th>
|
||||
<td>{{ $contact->created_at->format('d.m.Y H:i') }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>Aktualisiert:</th>
|
||||
<td>{{ $contact->updated_at->format('d.m.Y H:i') }}</td>
|
||||
</tr>
|
||||
@if ($contact->subscribed_at)
|
||||
<tr>
|
||||
<th>Angemeldet:</th>
|
||||
<td>{{ $contact->subscribed_at->format('d.m.Y H:i') }}</td>
|
||||
</tr>
|
||||
@endif
|
||||
@if ($contact->unsubscribed_at)
|
||||
<tr>
|
||||
<th>Abgemeldet:</th>
|
||||
<td>{{ $contact->unsubscribed_at->format('d.m.Y H:i') }}</td>
|
||||
</tr>
|
||||
@endif
|
||||
@if ($contact->last_synced_at)
|
||||
<tr>
|
||||
<th>Letzte Sync:</th>
|
||||
<td>{{ $contact->last_synced_at->format('d.m.Y H:i') }}</td>
|
||||
</tr>
|
||||
@endif
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@endsection
|
||||
150
resources/views/newsletter/edit.blade.php
Normal file
|
|
@ -0,0 +1,150 @@
|
|||
@extends('layouts.layout-2')
|
||||
|
||||
@section('content')
|
||||
<h4 class="d-flex justify-content-between align-items-center w-100 font-weight-bold py-3 mb-4">
|
||||
<div>
|
||||
<i class="ion ion-md-create text-primary"></i>
|
||||
{{ $id === 'new' ? 'Neuer Newsletter-Kontakt' : 'Newsletter-Kontakt bearbeiten' }}
|
||||
</div>
|
||||
<div>
|
||||
@if ($id !== 'new')
|
||||
<a href="{{ route('newsletter.detail', $contact->id) }}" class="btn btn-secondary btn-sm">
|
||||
<i class="fa fa-arrow-left"></i> Zurück
|
||||
</a>
|
||||
@else
|
||||
<a href="{{ route('newsletter.index') }}" class="btn btn-secondary btn-sm">
|
||||
<i class="fa fa-arrow-left"></i> Zurück
|
||||
</a>
|
||||
@endif
|
||||
</div>
|
||||
</h4>
|
||||
|
||||
@if ($errors->any())
|
||||
<div class="alert alert-danger alert-dismissible fade show">
|
||||
<button type="button" class="close" data-dismiss="alert">×</button>
|
||||
<ul class="mb-0">
|
||||
@foreach ($errors->all() as $error)
|
||||
<li>{{ $error }}</li>
|
||||
@endforeach
|
||||
</ul>
|
||||
</div>
|
||||
@endif
|
||||
|
||||
<div class="card">
|
||||
<div class="card-body">
|
||||
<form method="POST" action="{{ route('newsletter.store', $id) }}">
|
||||
@csrf
|
||||
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
<div class="form-group">
|
||||
<label class="form-label">E-Mail-Adresse <span class="text-danger">*</span></label>
|
||||
<input type="email" name="email" class="form-control @error('email') is-invalid @enderror"
|
||||
value="{{ old('email', $contact->email) }}" required>
|
||||
@error('email')
|
||||
<div class="invalid-feedback">{{ $message }}</div>
|
||||
@enderror
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-md-3">
|
||||
<div class="form-group">
|
||||
<label class="form-label">Vorname</label>
|
||||
<input type="text" name="firstname" class="form-control"
|
||||
value="{{ old('firstname', $contact->firstname) }}">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-md-3">
|
||||
<div class="form-group">
|
||||
<label class="form-label">Nachname</label>
|
||||
<input type="text" name="lastname" class="form-control"
|
||||
value="{{ old('lastname', $contact->lastname) }}">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
<div class="form-group">
|
||||
<label class="form-label">Status <span class="text-danger">*</span></label>
|
||||
<select name="status" class="form-control @error('status') is-invalid @enderror" required>
|
||||
@foreach (\App\Models\NewsletterContact::$statusLabels as $key => $label)
|
||||
<option value="{{ $key }}"
|
||||
{{ old('status', $contact->status) === $key ? 'selected' : '' }}>
|
||||
{{ $label }}
|
||||
</option>
|
||||
@endforeach
|
||||
</select>
|
||||
@error('status')
|
||||
<div class="invalid-feedback">{{ $message }}</div>
|
||||
@enderror
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-md-6">
|
||||
<div class="form-group">
|
||||
<label class="form-label">Herkunft</label>
|
||||
<select name="source" class="form-control">
|
||||
@foreach (\App\Models\NewsletterContact::$sourceLabels as $key => $label)
|
||||
<option value="{{ $key }}"
|
||||
{{ old('source', $contact->source) === $key ? 'selected' : '' }}>
|
||||
{{ $label }}
|
||||
</option>
|
||||
@endforeach
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-md-12">
|
||||
<div class="form-group">
|
||||
<label class="form-label">Newsletter-Gruppen</label>
|
||||
<div class="custom-controls-stacked">
|
||||
<label class="custom-control custom-checkbox">
|
||||
<input type="checkbox" name="group_kulturreisen" class="custom-control-input"
|
||||
value="1"
|
||||
{{ old('group_kulturreisen', $contact->group_kulturreisen) ? 'checked' : '' }}>
|
||||
<span class="custom-control-label">Kulturreisen</span>
|
||||
</label>
|
||||
<label class="custom-control custom-checkbox">
|
||||
<input type="checkbox" name="group_ferienwohnungen" class="custom-control-input"
|
||||
value="1"
|
||||
{{ old('group_ferienwohnungen', $contact->group_ferienwohnungen) ? 'checked' : '' }}>
|
||||
<span class="custom-control-label">Ferienwohnungen</span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-md-12">
|
||||
<div class="form-group">
|
||||
<label class="form-label">Notizen</label>
|
||||
<textarea name="notes" class="form-control" rows="4">{{ old('notes', $contact->notes) }}</textarea>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<hr class="my-4">
|
||||
|
||||
<div class="text-right">
|
||||
@if ($id !== 'new')
|
||||
<a href="{{ route('newsletter.detail', $contact->id) }}" class="btn btn-secondary">
|
||||
Abbrechen
|
||||
</a>
|
||||
@else
|
||||
<a href="{{ route('newsletter.index') }}" class="btn btn-secondary">
|
||||
Abbrechen
|
||||
</a>
|
||||
@endif
|
||||
<button type="submit" class="btn btn-primary">
|
||||
<i class="fa fa-save"></i> Speichern
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
@endsection
|
||||
322
resources/views/newsletter/index.blade.php
Normal file
|
|
@ -0,0 +1,322 @@
|
|||
@extends('layouts.layout-2')
|
||||
|
||||
@section('content')
|
||||
<h4 class="d-flex justify-content-between align-items-center w-100 font-weight-bold py-3 mb-4">
|
||||
<div>
|
||||
<i class="ion ion-md-mail text-primary"></i> Newsletter-Kontakte
|
||||
</div>
|
||||
<div>
|
||||
<a href="{{ route('newsletter.edit', 'new') }}" class="btn btn-primary btn-sm">
|
||||
<i class="fa fa-plus"></i> Neuer Kontakt
|
||||
</a>
|
||||
</div>
|
||||
</h4>
|
||||
|
||||
@if (Session::has('alert-success'))
|
||||
<div class="alert alert-success alert-dismissible fade show">
|
||||
<button type="button" class="close" data-dismiss="alert">×</button>
|
||||
{{ Session::get('alert-success') }}
|
||||
</div>
|
||||
@endif
|
||||
|
||||
<!-- Statistiken -->
|
||||
<div class="row">
|
||||
<div class="col-sm-6 col-xl-3">
|
||||
<div class="card mb-4">
|
||||
<div class="card-body">
|
||||
<div class="d-flex align-items-center">
|
||||
<div class="lnr lnr-users display-4 text-primary"></div>
|
||||
<div class="ml-3">
|
||||
<div class="text-muted small">Gesamte Kontakte</div>
|
||||
<div class="text-large">{{ $statistics['total'] }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-sm-6 col-xl-3">
|
||||
<div class="card mb-4">
|
||||
<div class="card-body">
|
||||
<div class="d-flex align-items-center">
|
||||
<div class="lnr lnr-checkmark-circle display-4 text-success"></div>
|
||||
<div class="ml-3">
|
||||
<div class="text-muted small">Aktive Kontakte</div>
|
||||
<div class="text-large">{{ $statistics['active'] }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-sm-6 col-xl-3">
|
||||
<div class="card mb-4">
|
||||
<div class="card-body">
|
||||
<div class="d-flex align-items-center">
|
||||
<div class="lnr lnr-map display-4 text-info"></div>
|
||||
<div class="ml-3">
|
||||
<div class="text-muted small">Kulturreisen</div>
|
||||
<div class="text-large">{{ $statistics['kulturreisen'] }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-sm-6 col-xl-3">
|
||||
<div class="card mb-4">
|
||||
<div class="card-body">
|
||||
<div class="d-flex align-items-center">
|
||||
<div class="lnr lnr-home display-4 text-primary"></div>
|
||||
<div class="ml-3">
|
||||
<div class="text-muted small">Ferienwohnungen</div>
|
||||
<div class="text-large">{{ $statistics['ferienwohnungen'] }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Filter & Aktionen -->
|
||||
<div class="card mb-4">
|
||||
<div class="card-body">
|
||||
<div class="row">
|
||||
<div class="col-md-3">
|
||||
<label>Gruppe</label>
|
||||
<select class="form-control" id="filter-group">
|
||||
<option value="">Alle Gruppen</option>
|
||||
<option value="kulturreisen">Kulturreisen</option>
|
||||
<option value="ferienwohnungen">Ferienwohnungen</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<label>Status</label>
|
||||
<select class="form-control" id="filter-status">
|
||||
<option value="">Alle Status</option>
|
||||
<option value="active">Aktiv</option>
|
||||
<option value="inactive">Inaktiv</option>
|
||||
<option value="unsubscribed">Abgemeldet</option>
|
||||
<option value="bounced">Bounced</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<label>Herkunft</label>
|
||||
<select class="form-control" id="filter-source">
|
||||
<option value="">Alle Herkünfte</option>
|
||||
<option value="booking_kulturreisen">Buchung Kulturreisen</option>
|
||||
<option value="booking_ferienwohnungen">Buchung Ferienwohnungen</option>
|
||||
<option value="newsletter_signup">Newsletter-Anmeldung</option>
|
||||
<option value="manual">Manuell</option>
|
||||
<option value="import">Import</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<label> </label>
|
||||
<button type="button" class="btn btn-primary btn-block" id="btn-filter">
|
||||
<i class="fa fa-filter"></i> Filtern
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row mt-3">
|
||||
<div class="col-md-3">
|
||||
<label>Letzte Reise von</label>
|
||||
<input type="text" class="form-control datepicker-base" id="filter-travel-from"
|
||||
placeholder="TT.MM.JJJJ">
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<label>Letzte Reise bis</label>
|
||||
<input type="text" class="form-control datepicker-base" id="filter-travel-to"
|
||||
placeholder="TT.MM.JJJJ">
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<label> </label>
|
||||
<button type="button" class="btn btn-secondary btn-block" id="btn-reset-filter">
|
||||
<i class="fa fa-times"></i> Filter zurücksetzen
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row mt-3">
|
||||
<div class="col-md-12">
|
||||
<div class="btn-group">
|
||||
<button type="button" class="btn btn-success btn-sm" onclick="syncContacts('all', false)">
|
||||
<i class="fa fa-sync"></i> Synchronisieren
|
||||
</button>
|
||||
<button type="button" class="btn btn-warning btn-sm" onclick="syncContacts('all', true)">
|
||||
<i class="fa fa-sync"></i> Voll-Sync (alle)
|
||||
</button>
|
||||
</div>
|
||||
<button type="button" class="btn btn-info btn-sm ml-2" onclick="exportContacts()">
|
||||
<i class="fa fa-download"></i> Export (mit aktuellen Filtern)
|
||||
</button>
|
||||
@if ($statistics['last_sync'])
|
||||
<small class="text-muted ml-3">
|
||||
Letzte Synchronisation: {{ $statistics['last_sync'] }}
|
||||
</small>
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Tabelle -->
|
||||
<div class="card">
|
||||
<div class="card-body">
|
||||
<div class="table-responsive">
|
||||
<table class="table table-striped table-bordered" id="newsletter-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>ID</th>
|
||||
<th>E-Mail</th>
|
||||
<th>Name</th>
|
||||
<th>Gruppen</th>
|
||||
<th>Status</th>
|
||||
<th>Herkunft</th>
|
||||
<th>Buchungen</th>
|
||||
<th>Letzte Reise</th>
|
||||
<th>Letzte Buchung</th>
|
||||
<th>Erstellt</th>
|
||||
<th>Aktionen</th>
|
||||
</tr>
|
||||
</thead>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@endsection
|
||||
|
||||
@section('scripts')
|
||||
<script>
|
||||
$(document).ready(function() {
|
||||
var table = $('#newsletter-table').DataTable({
|
||||
processing: true,
|
||||
serverSide: true,
|
||||
ajax: {
|
||||
url: '{{ route('newsletter.datatable') }}',
|
||||
data: function(d) {
|
||||
d.group = $('#filter-group').val();
|
||||
d.status = $('#filter-status').val();
|
||||
d.source = $('#filter-source').val();
|
||||
d.travel_from = $('#filter-travel-from').val();
|
||||
d.travel_to = $('#filter-travel-to').val();
|
||||
}
|
||||
},
|
||||
columns: [{
|
||||
data: 'id',
|
||||
name: 'id'
|
||||
},
|
||||
{
|
||||
data: 'email',
|
||||
name: 'email'
|
||||
},
|
||||
{
|
||||
data: 'full_name',
|
||||
name: 'full_name',
|
||||
orderable: false,
|
||||
searchable: false
|
||||
},
|
||||
{
|
||||
data: 'groups',
|
||||
name: 'groups',
|
||||
orderable: false,
|
||||
searchable: false
|
||||
},
|
||||
{
|
||||
data: 'status_badge',
|
||||
name: 'status'
|
||||
},
|
||||
{
|
||||
data: 'source_label',
|
||||
name: 'source_label'
|
||||
},
|
||||
{
|
||||
data: 'total_bookings',
|
||||
name: 'total_bookings',
|
||||
orderable: false,
|
||||
searchable: false
|
||||
},
|
||||
{
|
||||
data: 'last_travel',
|
||||
name: 'last_travel_end_date'
|
||||
},
|
||||
{
|
||||
data: 'last_booking',
|
||||
name: 'last_booking_at'
|
||||
},
|
||||
{
|
||||
data: 'created',
|
||||
name: 'created_at'
|
||||
},
|
||||
{
|
||||
data: 'actions',
|
||||
name: 'actions',
|
||||
orderable: false,
|
||||
searchable: false
|
||||
}
|
||||
],
|
||||
order: [
|
||||
[0, 'desc']
|
||||
],
|
||||
iDisplayLength: 50,
|
||||
language: {
|
||||
url: '//cdn.datatables.net/plug-ins/1.10.24/i18n/German.json'
|
||||
}
|
||||
});
|
||||
|
||||
$('#btn-filter').on('click', function() {
|
||||
table.draw();
|
||||
});
|
||||
|
||||
$('#btn-reset-filter').on('click', function() {
|
||||
$('#filter-group').val('');
|
||||
$('#filter-status').val('');
|
||||
$('#filter-source').val('');
|
||||
$('#filter-travel-from').val('');
|
||||
$('#filter-travel-to').val('');
|
||||
table.draw();
|
||||
});
|
||||
});
|
||||
|
||||
function syncContacts(type, force) {
|
||||
if (!confirm('Möchten Sie die Synchronisation wirklich starten?')) {
|
||||
return;
|
||||
}
|
||||
|
||||
var url = '{{ route('newsletter.sync') }}';
|
||||
var data = {
|
||||
type: type,
|
||||
_token: '{{ csrf_token() }}'
|
||||
};
|
||||
|
||||
if (force) {
|
||||
data.force = true;
|
||||
}
|
||||
|
||||
$.post(url, data, function(response) {
|
||||
location.reload();
|
||||
}).fail(function() {
|
||||
alert('Fehler bei der Synchronisation');
|
||||
});
|
||||
}
|
||||
|
||||
function exportContacts() {
|
||||
var params = [];
|
||||
|
||||
var group = $('#filter-group').val();
|
||||
var status = $('#filter-status').val();
|
||||
var source = $('#filter-source').val();
|
||||
var travelFrom = $('#filter-travel-from').val();
|
||||
var travelTo = $('#filter-travel-to').val();
|
||||
|
||||
if (group) params.push('group=' + encodeURIComponent(group));
|
||||
if (status) params.push('status=' + encodeURIComponent(status));
|
||||
if (source) params.push('source=' + encodeURIComponent(source));
|
||||
if (travelFrom) params.push('travel_from=' + encodeURIComponent(travelFrom));
|
||||
if (travelTo) params.push('travel_to=' + encodeURIComponent(travelTo));
|
||||
|
||||
var url = '{{ route('newsletter.export') }}';
|
||||
if (params.length > 0) {
|
||||
url += '?' + params.join('&');
|
||||
}
|
||||
|
||||
window.location.href = url;
|
||||
}
|
||||
</script>
|
||||
@endsection
|
||||
|
|
@ -12,6 +12,7 @@ use Illuminate\Http\Request;
|
|||
| is assigned the "api" middleware group. Enjoy building your API!
|
||||
|
|
||||
*/
|
||||
|
||||
Route::post('login', 'API\UserController@login');
|
||||
Route::get('test', 'API\UserController@test');
|
||||
|
||||
|
|
@ -22,6 +23,14 @@ Route::post('cms/keywords', 'API\CMSContentController@keywords');
|
|||
Route::post('cms/header/info', 'API\CMSContentInfoController@headerInfo');
|
||||
Route::get('cms/header/info', 'API\CMSContentInfoController@headerInfo');
|
||||
|
||||
// Navigation API Routes
|
||||
Route::get('navigation/tree', 'API\NavigationController@getNavigationTree');
|
||||
Route::get('navigation/tree/active', 'API\NavigationController@getActiveNavigationTree');
|
||||
Route::get('navigation/tree/{rootId}', 'API\NavigationController@getNavigationSubTree');
|
||||
Route::get('navigation/flat', 'API\NavigationController@getFlatNavigationList');
|
||||
Route::get('navigation/breadcrumb/{pageId}', 'API\NavigationController@getBreadcrumb');
|
||||
Route::post('navigation/cache/clear', 'API\NavigationController@clearCache');
|
||||
|
||||
Route::get('passolution/{lang}/{nat}/{destco}/{tdat}', 'API\CMSContentController@passolution');
|
||||
Route::post('passolution/{lang}', 'API\CMSContentController@passolutionPost');
|
||||
|
||||
|
|
@ -29,10 +38,10 @@ Route::post('passolution/{lang}', 'API\CMSContentController@passolutionPost');
|
|||
Route::post('booking/import', 'API\BookingController@import');
|
||||
Route::get('booking/import', 'API\BookingController@import');
|
||||
|
||||
Route::group(['middleware' => 'auth:api'], function(){
|
||||
Route::group(['middleware' => 'auth:api'], function () {
|
||||
Route::post('details', 'API\UserController@details');
|
||||
Route::post('draft/{action}', 'API\DraftController@draft');
|
||||
Route::post('load/{action}', 'API\LoaderController@load');
|
||||
Route::post('fewo/{action}', 'API\FewoController@action');
|
||||
Route::post('lead/{action}', 'API\LeadController@action');
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -130,6 +130,21 @@ Route::group(['middleware' => ['admin', '2fa']], function () {
|
|||
Route::get('/iq/content/assets', 'IQ\ContentAssetController@index')->name('iq_content_assets');
|
||||
Route::get('/iq/content/assets/modal', 'IQ\ContentAssetController@modal')->name('iq_content_assets_modal');
|
||||
});
|
||||
|
||||
// Newsletter Routes
|
||||
Route::group(['middleware' => ['auth.permission:cms-newsletter']], function () {
|
||||
Route::get('/newsletter', 'NewsletterController@index')->name('newsletter.index');
|
||||
Route::get('/newsletter/datatable', 'NewsletterController@getDatatable')->name('newsletter.datatable');
|
||||
Route::get('/newsletter/{id}', 'NewsletterController@detail')->name('newsletter.detail')->where('id', '[0-9]+');
|
||||
Route::get('/newsletter/{id}/edit', 'NewsletterController@edit')->name('newsletter.edit');
|
||||
Route::post('/newsletter/{id}/store', 'NewsletterController@store')->name('newsletter.store');
|
||||
Route::delete('/newsletter/{id}', 'NewsletterController@delete')->name('newsletter.delete');
|
||||
Route::post('/newsletter/{id}/unsubscribe', 'NewsletterController@unsubscribe')->name('newsletter.unsubscribe');
|
||||
Route::post('/newsletter/{id}/resubscribe', 'NewsletterController@resubscribe')->name('newsletter.resubscribe');
|
||||
Route::post('/newsletter/sync', 'NewsletterController@sync')->name('newsletter.sync');
|
||||
Route::get('/newsletter/export', 'NewsletterController@export')->name('newsletter.export');
|
||||
});
|
||||
|
||||
Route::group(['middleware' => ['auth.permission:crm-tp-pr']], function () {
|
||||
//Reiseprogramme Programme
|
||||
Route::get('/travel/programs/{step?}', 'TravelProgramController@index')->name('travel_programs');
|
||||
|
|
@ -370,6 +385,15 @@ Route::group(['middleware' => ['admin', '2fa']], function () {
|
|||
Route::post('/cms/sidebar/detail/{id}', 'CMS\CMSSidebarController@store')->name('cms_sidebar_detail');
|
||||
Route::get('/cms/sidebar/delete/{id}', 'CMS\CMSSidebarController@delete')->name('cms_sidebar_delete');
|
||||
});
|
||||
Route::group(['middleware' => ['auth.permission:crm-nav-api']], function () {
|
||||
// Navigation API
|
||||
Route::get('/navigation-api', 'NavigationTreeController@index')->name('navigation_api');
|
||||
Route::get('/navigation-api/data', 'NavigationTreeController@getData')->name('navigation_api_data');
|
||||
Route::get('/navigation-api/search', 'NavigationTreeController@search')->name('navigation_api_search');
|
||||
Route::get('/navigation-api/export', 'NavigationTreeController@export')->name('navigation_api_export');
|
||||
Route::post('/navigation-api/clear-cache', 'NavigationTreeController@clearCache')->name('navigation_api_clear_cache');
|
||||
Route::get('/navigation-api/stats', 'NavigationTreeController@stats')->name('navigation_api_stats');
|
||||
});
|
||||
});
|
||||
|
||||
//login pages for worker
|
||||
|
|
|
|||