WIP: Sicherheitsnetz vor Phase-1-R\u00fcckbau

Enth\u00e4lt gemischt: Laravel-10-Upgrade + Phase 1 (Contacts-Modul, Duplicats-Commands,
Soft-Delete+Merge-Fields) + Phase 2 Code-Umstellungen (inquiry_id, $table='contacts'/'inquiries')
+ Offers-Modul (Migrationen, Models, offer_id in Booking, offer-Disk in filesystems.php).

Phase 2 + Offers werden im folgenden Commit nach dev/backups/phase2-offers-2026-04-17/
verschoben, damit der Workspace auf Phase-1-only (= Test-System-Stand) reduziert ist
und direkt auf Live deploybar wird.

Tarball-Backup zus\u00e4tzlich unter: ../backups-safety/workspace-pre-phase1-rollback-2026-04-17.tar.gz

Made-with: Cursor
This commit is contained in:
Phase-1-Rollback-Agent 2026-04-17 13:40:31 +00:00
parent 389d5d1820
commit e3dc1afd8e
165 changed files with 21914 additions and 3516 deletions

View file

@ -0,0 +1,160 @@
<?php
namespace App\Console\Commands;
use Illuminate\Console\Command;
use Illuminate\Support\Facades\DB;
/**
* Phase 1 Schritt 1: Duplikate identifizieren
*
* Sucht Kunden-Datensätze, die vermutlich dieselbe Person repräsentieren.
* Drei Erkennungs-Stufen (absteigend nach Konfidenz):
*
* HIGH gleiche E-Mail (nicht leer)
* MEDIUM gleicher Name + Vorname + Geburtsdatum
* LOW gleicher Name + Vorname + PLZ
*
* Verwendung:
* php artisan contacts:find-duplicates
* php artisan contacts:find-duplicates --export=duplicates.csv
* php artisan contacts:find-duplicates --confidence=HIGH
*/
class ContactsFindDuplicates extends Command
{
protected $signature = 'contacts:find-duplicates
{--export= : Pfad zur CSV-Ausgabedatei}
{--confidence= : Nur diese Konfidenz-Stufe ausgeben (HIGH|MEDIUM|LOW)}';
protected $description = 'Identifiziert doppelte Customer-Datensätze anhand von E-Mail, Name/Geburtsdatum oder Name/PLZ';
public function handle(): int
{
$this->info('Suche nach Duplikaten in der customer-Tabelle...');
$this->newLine();
$groups = collect();
// ── HIGH: gleiche E-Mail ──────────────────────────────────────────
if ($this->shouldCheck('HIGH')) {
$emailDupes = DB::table('contacts')
->select('email', DB::raw('COUNT(*) as cnt'), DB::raw('GROUP_CONCAT(id ORDER BY updated_at DESC) as ids'))
->whereNotNull('email')
->where('email', '!=', '')
->whereNull('merged_into_id')
->groupBy('email')
->having('cnt', '>', 1)
->get();
foreach ($emailDupes as $row) {
$groups->push([
'confidence' => 'HIGH',
'reason' => 'E-Mail: ' . $row->email,
'ids' => $row->ids,
'count' => $row->cnt,
]);
}
$this->line(sprintf('<fg=green>HIGH</> (gleiche E-Mail): %d Gruppen', $emailDupes->count()));
}
// ── MEDIUM: Name + Vorname + Geburtsdatum ────────────────────────
if ($this->shouldCheck('MEDIUM')) {
$nameBdDupes = DB::table('contacts')
->select(
'name', 'firstname', 'birthdate',
DB::raw('COUNT(*) as cnt'),
DB::raw('GROUP_CONCAT(id ORDER BY updated_at DESC) as ids')
)
->whereNotNull('name')
->whereNotNull('firstname')
->whereNotNull('birthdate')
->whereNull('merged_into_id')
->groupBy('name', 'firstname', 'birthdate')
->having('cnt', '>', 1)
->get();
foreach ($nameBdDupes as $row) {
$groups->push([
'confidence' => 'MEDIUM',
'reason' => "Name: {$row->firstname} {$row->name}, GD: {$row->birthdate}",
'ids' => $row->ids,
'count' => $row->cnt,
]);
}
$this->line(sprintf('<fg=yellow>MEDIUM</> (Name+GD): %d Gruppen', $nameBdDupes->count()));
}
// ── LOW: Name + Vorname + PLZ ─────────────────────────────────────
if ($this->shouldCheck('LOW')) {
$nameZipDupes = DB::table('contacts')
->select(
'name', 'firstname', 'zip',
DB::raw('COUNT(*) as cnt'),
DB::raw('GROUP_CONCAT(id ORDER BY updated_at DESC) as ids')
)
->whereNotNull('name')
->whereNotNull('firstname')
->whereNotNull('zip')
->where('zip', '!=', '')
->whereNull('merged_into_id')
->groupBy('name', 'firstname', 'zip')
->having('cnt', '>', 1)
->get();
foreach ($nameZipDupes as $row) {
$groups->push([
'confidence' => 'LOW',
'reason' => "Name: {$row->firstname} {$row->name}, PLZ: {$row->zip}",
'ids' => $row->ids,
'count' => $row->cnt,
]);
}
$this->line(sprintf('<fg=red>LOW</> (Name+PLZ): %d Gruppen', $nameZipDupes->count()));
}
$this->newLine();
$this->info(sprintf('Gesamt: %d Duplikat-Gruppen gefunden', $groups->count()));
if ($groups->isEmpty()) {
$this->info('Keine Duplikate — nichts zu tun.');
return self::SUCCESS;
}
// Tabellen-Ausgabe
$this->table(
['Konfidenz', 'Grund', 'IDs (neueste zuerst)', 'Anzahl'],
$groups->map(fn ($g) => [$g['confidence'], $g['reason'], $g['ids'], $g['count']])->all()
);
// CSV-Export
if ($export = $this->option('export')) {
$this->exportCsv($groups->all(), $export);
$this->info("CSV gespeichert: {$export}");
} else {
$this->newLine();
$this->line('Tipp: --export=duplicates.csv für CSV-Export');
$this->line('Tipp: php artisan contacts:merge-duplicates --dry-run zum Prüfen');
}
return self::SUCCESS;
}
private function shouldCheck(string $level): bool
{
$filter = strtoupper((string) $this->option('confidence'));
return $filter === '' || $filter === $level;
}
private function exportCsv(array $groups, string $path): void
{
$handle = fopen($path, 'w');
fputcsv($handle, ['Konfidenz', 'Grund', 'IDs (neueste zuerst)', 'Anzahl']);
foreach ($groups as $group) {
fputcsv($handle, [$group['confidence'], $group['reason'], $group['ids'], $group['count']]);
}
fclose($handle);
}
}

View file

@ -0,0 +1,233 @@
<?php
namespace App\Console\Commands;
use Illuminate\Console\Command;
use Illuminate\Support\Facades\DB;
/**
* Phase 1 Schritt 2: Duplikate zusammenführen
*
* Strategie: Der neueste Datensatz (höchstes updated_at, dann höchste id)
* wird Master. Alle anderen Datensätze derselben Gruppe erhalten
* merged_into_id = master_id und werden nicht mehr zurückgegeben.
*
* Alle FK-Referenzen in lead, booking, customer_mails, lead_mails
* werden auf den Master umgestellt.
*
* Verwendung:
* php artisan contacts:merge-duplicates --dry-run # Vorschau, keine Änderung
* php artisan contacts:merge-duplicates --confidence=HIGH # Nur sichere Duplikate
* php artisan contacts:merge-duplicates # Ausführen
*/
class ContactsMergeDuplicates extends Command
{
protected $signature = 'contacts:merge-duplicates
{--dry-run : Zeigt was passieren würde, ohne Daten zu ändern}
{--confidence= : Nur diese Konfidenz-Stufe verarbeiten (HIGH|MEDIUM|LOW)}
{--force : Überspringt Sicherheitsabfrage}';
protected $description = 'Führt doppelte Customer-Datensätze zusammen (neuester wird Master)';
private bool $dryRun = false;
private int $mergedCount = 0;
private int $updatedLeads = 0;
private int $updatedBookings = 0;
public function handle(): int
{
$this->dryRun = (bool) $this->option('dry-run');
if ($this->dryRun) {
$this->warn('DRY-RUN Modus — keine Daten werden verändert');
} else {
$this->warn('ACHTUNG: Diese Operation verändert Produktionsdaten.');
if (!$this->option('force') && !$this->confirm('Fortfahren?')) {
$this->info('Abgebrochen.');
return self::SUCCESS;
}
}
$this->newLine();
DB::transaction(function () {
$this->processLevel(
'HIGH',
fn () => $this->findByEmail()
);
$this->processLevel(
'MEDIUM',
fn () => $this->findByNameBirthdate()
);
$this->processLevel(
'LOW',
fn () => $this->findByNameZip()
);
});
$this->newLine();
$this->info(sprintf(
'%s %d Duplikate zusammengeführt | %d leads aktualisiert | %d bookings aktualisiert',
$this->dryRun ? '[DRY-RUN]' : '',
$this->mergedCount,
$this->updatedLeads,
$this->updatedBookings
));
if ($this->dryRun) {
$this->newLine();
$this->line('Zum Ausführen: php artisan contacts:merge-duplicates');
}
return self::SUCCESS;
}
// ─────────────────────────────────────────────────────────────────────────
// Duplikat-Gruppen ermitteln
// ─────────────────────────────────────────────────────────────────────────
private function findByEmail(): array
{
return DB::table('contacts')
->select('email', DB::raw('GROUP_CONCAT(id ORDER BY updated_at DESC, id DESC) as ids'))
->whereNotNull('email')
->where('email', '!=', '')
->whereNull('merged_into_id')
->groupBy('email')
->having(DB::raw('COUNT(*)'), '>', 1)
->pluck('ids')
->map(fn ($ids) => explode(',', $ids))
->all();
}
private function findByNameBirthdate(): array
{
return DB::table('contacts')
->select(DB::raw('GROUP_CONCAT(id ORDER BY updated_at DESC, id DESC) as ids'))
->whereNotNull('name')
->whereNotNull('firstname')
->whereNotNull('birthdate')
->whereNull('merged_into_id')
->groupBy('name', 'firstname', 'birthdate')
->having(DB::raw('COUNT(*)'), '>', 1)
->pluck('ids')
->map(fn ($ids) => explode(',', $ids))
->all();
}
private function findByNameZip(): array
{
return DB::table('contacts')
->select(DB::raw('GROUP_CONCAT(id ORDER BY updated_at DESC, id DESC) as ids'))
->whereNotNull('name')
->whereNotNull('firstname')
->whereNotNull('zip')
->where('zip', '!=', '')
->whereNull('merged_into_id')
->groupBy('name', 'firstname', 'zip')
->having(DB::raw('COUNT(*)'), '>', 1)
->pluck('ids')
->map(fn ($ids) => explode(',', $ids))
->all();
}
// ─────────────────────────────────────────────────────────────────────────
// Verarbeitung
// ─────────────────────────────────────────────────────────────────────────
private function processLevel(string $level, callable $finder): void
{
if ($filter = $this->option('confidence')) {
if (strtoupper($filter) !== $level) {
return;
}
}
$groups = $finder();
if (empty($groups)) {
$this->line("[{$level}] Keine Duplikate.");
return;
}
$this->line(sprintf('[%s] %d Gruppe(n) gefunden', $level, count($groups)));
foreach ($groups as $ids) {
$masterId = (int) $ids[0]; // erster = neuester
$duplicateIds = array_map('intval', array_slice($ids, 1));
$this->line(sprintf(
' Master: #%d ← Duplikate: %s',
$masterId,
implode(', ', array_map(fn ($id) => '#' . $id, $duplicateIds))
));
foreach ($duplicateIds as $dupeId) {
$this->mergeInto($masterId, $dupeId);
}
}
}
private function mergeInto(int $masterId, int $dupeId): void
{
// 1. Leads umhängen
$leadCount = DB::table('inquiries')->where('customer_id', $dupeId)->count();
if ($leadCount > 0) {
$this->line(" lead.customer_id: {$leadCount} Zeile(n) → #{$masterId}");
if (!$this->dryRun) {
DB::table('inquiries')
->where('customer_id', $dupeId)
->update(['customer_id' => $masterId]);
}
$this->updatedLeads += $leadCount;
}
// 2. Bookings umhängen
$bookingCount = DB::table('booking')->where('customer_id', $dupeId)->count();
if ($bookingCount > 0) {
$this->line(" booking.customer_id: {$bookingCount} Zeile(n) → #{$masterId}");
if (!$this->dryRun) {
DB::table('booking')
->where('customer_id', $dupeId)
->update(['customer_id' => $masterId]);
}
$this->updatedBookings += $bookingCount;
}
// 3. customer_mails umhängen
$mailCount = DB::table('customer_mails')->where('customer_id', $dupeId)->count();
if ($mailCount > 0) {
$this->line(" customer_mails.customer_id: {$mailCount} Zeile(n) → #{$masterId}");
if (!$this->dryRun) {
DB::table('customer_mails')
->where('customer_id', $dupeId)
->update(['customer_id' => $masterId]);
}
}
// 4. lead_mails umhängen
$leadMailCount = DB::table('lead_mails')->where('customer_id', $dupeId)->count();
if ($leadMailCount > 0) {
$this->line(" lead_mails.customer_id: {$leadMailCount} Zeile(n) → #{$masterId}");
if (!$this->dryRun) {
DB::table('lead_mails')
->where('customer_id', $dupeId)
->update(['customer_id' => $masterId]);
}
}
// 5. Duplikat als zusammengeführt markieren
if (!$this->dryRun) {
DB::table('contacts')
->where('id', $dupeId)
->update([
'merged_into_id' => $masterId,
'merged_at' => now(),
]);
}
$this->mergedCount++;
}
}

View file

@ -193,7 +193,8 @@ class SyncNewsletterKulturreisen extends Command
'description' => 'Kontakt durch Kulturreisen-Buchung erstellt',
'metadata' => [
'booking_id' => $booking->id,
'lead_id' => $booking->lead_id,
// Metadaten-Key bleibt `lead_id`; Wert kommt aus booking.inquiry_id.
'lead_id' => $booking->inquiry_id,
],
]);
}

View file

@ -22,10 +22,14 @@ class Kernel extends ConsoleKernel
* @param \Illuminate\Console\Scheduling\Schedule $schedule
* @return void
*/
protected function schedule(Schedule $schedule)
protected function schedule(Schedule $schedule): void
{
// $schedule->command('inspire')
// ->hourly();
// Duplikate täglich um 02:00 Uhr automatisch zusammenführen (nur HIGH-Konfidenz = exakte E-Mail-Treffer)
$schedule->command('contacts:merge-duplicates --confidence=HIGH --force')
->dailyAt('02:00')
->withoutOverlapping()
->runInBackground()
->appendOutputTo(storage_path('logs/contacts-merge.log'));
}
/**
@ -35,7 +39,7 @@ class Kernel extends ConsoleKernel
*/
protected function commands()
{
$this->load(__DIR__.'/Commands');
$this->load(__DIR__ . '/Commands');
require base_path('routes/console.php');
}

View file

@ -8,42 +8,37 @@ use App\Services\BookingImport;
class BookingController extends Controller
{
private $successStatus = 200;
private $successKey = 'f6077389c9ce710e554763a5de02c8ec';
protected $draftRepo;
private int $successStatus = 200;
private string $successKey;
public function __construct()
{
$this->successKey = config('app.success_key');
}
public function import()
{
$request = \Request::all();
if(!isset($request['key']) || $request['key'] !== $this->successKey){
if (!isset($request['key']) || $request['key'] !== $this->successKey) {
return response()->json(['error' => "key"], 401);
}
$travel_booking = TravelBooking::find($request['travel_booking_id']);
//# vor testing
//$travel_booking = TravelBooking::find(2922);
if(!isset($travel_booking) || !$travel_booking){
if (!$travel_booking) {
return response()->json(['error' => 'no-booking-found'], $this->successStatus);
}
$booking = BookingImport::importFrom($travel_booking);
$ret= [
'url_v1' => make_old_url('/index.php/booking/'.$booking->id.'/edit'),
$ret = [
'url_v1' => make_old_url('/index.php/booking/' . $booking->id . '/edit'),
'url_v3' => route('booking_detail', $booking->id),
'lead_id' => $booking->lead_id
// API-Feld bleibt `lead_id` aus Abwärtskompatibilität für API-Konsumenten;
// Wert kommt nach Modul 3 Phase 2 aus booking.inquiry_id.
'lead_id' => $booking->inquiry_id
];
return response()->json(['success' => "import", "ret" => $ret], $this->successStatus);
//return response()->json(['error' => 'no-node'], $this->successStatus);
}
}

View file

@ -400,10 +400,10 @@ class ReportController extends Controller
$price = 0;
$isset = [];
foreach ($all as $v){
if(!in_array($v->booking->lead_id, $isset)){
if(!in_array($v->booking->inquiry_id, $isset)){
$price += $v->booking->isCanceled() ? $v->booking->getPriceCanceledRaw() : $v->booking->getPriceRaw();
}
$isset[] = $v->booking->lead_id;
$isset[] = $v->booking->inquiry_id;
}
return Util::_number_format($price);
@ -416,8 +416,8 @@ class ReportController extends Controller
$price = 0;
$isset = [];
foreach ($all as $v){
$price += in_array($v->booking->lead_id, $isset) ? 0 : $v->booking->getPriceTotalRaw();
$isset[] = $v->booking->lead_id;
$price += in_array($v->booking->inquiry_id, $isset) ? 0 : $v->booking->getPriceTotalRaw();
$isset[] = $v->booking->inquiry_id;
}
return Util::_number_format($price);
})
@ -429,8 +429,8 @@ class ReportController extends Controller
$proceeds = 0;
$isset = [];
foreach ($all as $v){
$proceeds += in_array($v->booking->lead_id, $isset) ? 0 : $v->booking->proceeds(true);
$isset[] = $v->booking->lead_id;
$proceeds += in_array($v->booking->inquiry_id, $isset) ? 0 : $v->booking->proceeds(true);
$isset[] = $v->booking->inquiry_id;
}
return Util::_number_format($proceeds);
/*$all = $query->get();
@ -562,7 +562,7 @@ class ReportController extends Controller
$ctemps[$export->booking->id][] = array(
'Zähler' => $new ? $export->getCounter() : "",
'MyJack Nr.' => $new ? $export->booking->merlin_order_number : "",
'CRM Nr' => $new ? $export->booking->lead_id : "",
'CRM Nr' => $new ? $export->booking->inquiry_id : "",
'Kunde' => $new ? $export->booking->customer->name : "",
'Reisedatum' => $new ? $export->booking->getStartDateFormat() : "",
'Organisation' => $new ? ($export->booking->isCanceled() ? $export->booking->price_canceled : $export->booking->price) : "",
@ -630,7 +630,7 @@ class ReportController extends Controller
$ctemps[$export->booking->id][] = array(
'Zähler' => $new ? $export->getCounter() : "",
'MyJack Nr.' => $new ? $export->booking->merlin_order_number : "",
'CRM Nr' => $new ? $export->booking->lead_id : "",
'CRM Nr' => $new ? $export->booking->inquiry_id : "",
'Kunde' => $new ? $export->booking->customer->name : "",
'Reisedatum' => $new ? $export->booking->getStartDateFormat() : "",
'Reiseland' => $new && $export->booking->travel_country ? $export->booking->travel_country->name : "",

View file

@ -49,7 +49,7 @@ class ReportLeadsController extends Controller
{
$query = Lead::with('customer')->with('sf_guard_user')->with('status')
->select('lead.*');
->select('inquiries.*');
//->where('deleted_at', '=', null);
/*
@ -132,7 +132,7 @@ class ReportLeadsController extends Controller
// $q->select('sent_at')->where('sent_at', DB::raw("(select max('sent_at') customer_mails)")); //)
})->orderBy(
LeadMail::select('sent_at')
->whereColumn('lead_id', 'lead.id')
->whereColumn('lead_id', 'inquiries.id')
->orderBy('sent_at', 'DESC')
->limit(1)
, $order);

View file

@ -104,10 +104,10 @@ class ReportProviderController extends Controller
$price = 0;
$isset = [];
foreach ($all as $v){
if(!in_array($v->booking->lead_id, $isset)){
if(!in_array($v->booking->inquiry_id, $isset)){
$price += $v->booking->isCanceled() ? $v->booking->getPriceCanceledRaw() : $v->booking->getPriceRaw();
}
$isset[] = $v->booking->lead_id;
$isset[] = $v->booking->inquiry_id;
}
return Util::_number_format($price);
@ -120,8 +120,8 @@ class ReportProviderController extends Controller
$price = 0;
$isset = [];
foreach ($all as $v){
$price += in_array($v->booking->lead_id, $isset) ? 0 : $v->booking->getPriceTotalRaw();
$isset[] = $v->booking->lead_id;
$price += in_array($v->booking->inquiry_id, $isset) ? 0 : $v->booking->getPriceTotalRaw();
$isset[] = $v->booking->inquiry_id;
}
return Util::_number_format($price);
})
@ -133,8 +133,8 @@ class ReportProviderController extends Controller
$proceeds = 0;
$isset = [];
foreach ($all as $v){
$proceeds += in_array($v->booking->lead_id, $isset) ? 0 : $v->booking->proceeds(true);
$isset[] = $v->booking->lead_id;
$proceeds += in_array($v->booking->inquiry_id, $isset) ? 0 : $v->booking->proceeds(true);
$isset[] = $v->booking->inquiry_id;
}
return Util::_number_format($proceeds);
/*$all = $query->get();
@ -272,7 +272,7 @@ class ReportProviderController extends Controller
$ctemps[$export->booking->id][] = array(
'Zähler' => $new ? $export->getCounter() : "",
'MyJack Nr.' => $new ? $export->booking->merlin_order_number : "",
'CRM Nr' => $new ? $export->booking->lead_id : "",
'CRM Nr' => $new ? $export->booking->inquiry_id : "",
'Kunde' => $new ? $export->booking->customer->name : "",
'Reisedatum' => $new ? $export->booking->getStartDateFormat() : "",
'bis' => $new ? $export->booking->getEndDateFormat() : "",
@ -346,7 +346,7 @@ class ReportProviderController extends Controller
$ctemps[$export->booking->id][] = array(
'Zähler' => $new ? $export->getCounter() : "",
'MyJack Nr.' => $new ? $export->booking->merlin_order_number : "",
'CRM Nr' => $new ? $export->booking->lead_id : "",
'CRM Nr' => $new ? $export->booking->inquiry_id : "",
'Kunde' => $new ? $export->booking->customer->name : "",
'Reisedatum' => $new ? $export->booking->getStartDateFormat() : "",
'bis' => $new ? $export->booking->getEndDateFormat() : "",

View file

@ -2,7 +2,7 @@
namespace App\Http\Controllers;
use Request;
use Illuminate\Http\Request;
use App\Models\Booking;
use App\Models\BookingFile;
use App\Models\Participant;
@ -60,11 +60,9 @@ class BookingController extends Controller
return view('booking.detail', $data);
}
public function store($id)
public function store(Request $request, $id)
{
// \Session()->flash('alert-save', '1');
$data = Request::all();
$data = $request->all();
if ($id === "new") {
$booking = new Booking();
@ -308,11 +306,11 @@ class BookingController extends Controller
return $redirect;
}
public function loadModal()
public function loadModal(Request $request)
{
$data = Request::all();
$data = $request->all();
$ret = "";
if (Request::ajax()) {
if ($request->ajax()) {
if ($data['action'] === "new-customer-mail" || $data['action'] === "reply-customer-mail" || $data['action'] === "show-customer-mail" || $data['action'] === "edit-customer-mail") {
$data['customers'] = [];
if ($data['action'] === "new-customer-mail" && isset($data['booking_id']) && $booking = Booking::find($data['booking_id'])) {
@ -374,7 +372,7 @@ class BookingController extends Controller
$bookingFileRepo->_set('booking_id', $data['booking_id']);
$bookingFileRepo->_set('dir', '/files/' . date('Y/m') . '/');
$bookingFileRepo->_set('identifier', 'booking');
return $bookingFileRepo->uploadFile(Request::all());
return $bookingFileRepo->uploadFile($request->all());
}
}
}
@ -391,7 +389,7 @@ class BookingController extends Controller
return redirect(route('booking_detail', [$booking->id]) . "#collapseBookingOrganisation");
}
public function action($action, $id = false)
public function action(Request $request, $action, $id = false)
{
if (!$booking = Booking::find($id)) {
@ -400,7 +398,7 @@ class BookingController extends Controller
if ($action === 'change_travel_dates') {
$draftRepo = new DraftRepository($booking);
$draftRepo->change_dates_drafts_from_booking(Request::get('change_travel_start_date'));
$draftRepo->change_dates_drafts_from_booking($request->get('change_travel_start_date'));
\Session()->flash('alert-success', __('Datum der Reise wurde geändert'));
return redirect(route('booking_detail', [$booking->id]) . "#collapseBookingOrganisation");
}

View file

@ -0,0 +1,312 @@
<?php
namespace App\Http\Controllers;
use App\Models\Contact;
use App\Repositories\ContactRepository;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\DB;
use Yajra\DataTables\Facades\DataTables;
class ContactController extends Controller
{
public function __construct(private readonly ContactRepository $contactRepo)
{
$this->middleware(['admin', '2fa']);
}
public function index(): \Illuminate\View\View
{
return view('contact.index');
}
public function detail(string $id): \Illuminate\View\View
{
if ($id === 'new') {
$contact = new Contact();
} else {
$contact = Contact::with(['salutation', 'leads', 'bookings', 'mergedContacts'])
->findOrFail((int) $id);
}
return view('contact.detail', [
'contact' => $contact,
'id' => $id,
]);
}
public function store(Request $request, string $id): \Illuminate\Http\RedirectResponse
{
$data = $request->except('_token');
if (!isset($data['action'])) {
abort(403, 'keine Action');
}
if ($id === 'new') {
$contact = $this->contactRepo->createContact($data);
\Session()->flash('alert-save', '1');
return redirect(route('contact_detail', [$contact->id]) . '#collapseContactDetail');
}
$this->contactRepo->updateContact($id, $data);
\Session()->flash('alert-save', '1');
return redirect(route('contact_detail', [$id]) . '#collapseContactDetail');
}
public function history(int $id): \Illuminate\View\View
{
$contact = Contact::with([
'leads.sf_guard_user',
'bookings.travel_country',
'bookings.travel_agenda',
'bookings.sf_guard_user',
'bookings.lead',
])->findOrFail($id);
return view('contact._detail_history', [
'contact' => $contact,
'modal' => true,
]);
}
public function destroy(int $id): \Illuminate\Http\JsonResponse
{
$contact = Contact::withoutGlobalScope('not_merged')->findOrFail($id);
$leadsCount = $contact->leads()->count();
$bookingCount = $contact->bookings()->count();
if ($leadsCount > 0 || $bookingCount > 0) {
return response()->json([
'success' => false,
'message' => sprintf(
'Kontakt kann nicht gelöscht werden: %s%s vorhanden.',
$leadsCount > 0 ? $leadsCount . ' Anfrage(n)' : '',
$bookingCount > 0 ? ($leadsCount > 0 ? ' und ' : '') . $bookingCount . ' Buchung(en)' : ''
),
], 422);
}
$contact->delete();
return response()->json(['success' => true]);
}
public function duplicates(): \Illuminate\View\View
{
$counts = [
'HIGH' => $this->countDuplicateGroups('email'),
'MEDIUM' => $this->countDuplicateGroups('name_birthdate'),
'LOW' => $this->countDuplicateGroups('name_zip'),
];
return view('contact.duplicates', compact('counts'));
}
public function getDuplicateGroups(Request $request): \Illuminate\Http\JsonResponse
{
$confidence = strtoupper($request->input('confidence', 'HIGH'));
$groups = match ($confidence) {
'HIGH' => $this->findByEmail(),
'MEDIUM' => $this->findByNameBirthdate(),
'LOW' => $this->findByNameZip(),
default => [],
};
// Für jede Gruppe die vollständigen Kontakt-Daten laden
$result = [];
foreach ($groups as $ids) {
$contacts = Contact::withoutGlobalScopes()
->withCount(['leads', 'bookings'])
->whereIn('id', $ids)
->whereNull('merged_into_id')
->whereNull('deleted_at')
->orderByRaw('FIELD(id, ' . implode(',', $ids) . ')')
->get(['id', 'firstname', 'name', 'email', 'zip', 'city', 'phone', 'phonemobile', 'birthdate', 'created_at', 'updated_at']);
if ($contacts->count() < 2) {
continue;
}
$result[] = [
'master' => $contacts->first(),
'duplicates' => $contacts->skip(1)->values(),
];
}
return response()->json($result);
}
public function merge(Request $request): \Illuminate\Http\JsonResponse
{
$masterId = (int) $request->input('master_id');
$dupeId = (int) $request->input('duplicate_id');
if (!$masterId || !$dupeId || $masterId === $dupeId) {
return response()->json(['success' => false, 'message' => 'Ungültige IDs.'], 422);
}
$master = Contact::withoutGlobalScopes()->find($masterId);
$dupe = Contact::withoutGlobalScopes()->find($dupeId);
if (!$master || !$dupe) {
return response()->json(['success' => false, 'message' => 'Kontakt nicht gefunden.'], 404);
}
DB::transaction(function () use ($masterId, $dupeId) {
DB::table('inquiries')->where('customer_id', $dupeId)->update(['customer_id' => $masterId]);
DB::table('booking')->where('customer_id', $dupeId)->update(['customer_id' => $masterId]);
DB::table('customer_mails')->where('customer_id', $dupeId)->update(['customer_id' => $masterId]);
DB::table('lead_mails')->where('customer_id', $dupeId)->update(['customer_id' => $masterId]);
DB::table('contacts')->where('id', $dupeId)->update([
'merged_into_id' => $masterId,
'merged_at' => now(),
]);
});
return response()->json(['success' => true]);
}
// ── Duplikat-Hilfs-Queries ────────────────────────────────────────────────
private function countDuplicateGroups(string $type): int
{
return match ($type) {
'email' => DB::table('contacts')->selectRaw('COUNT(*) as cnt')->whereNotNull('email')->where('email', '!=', '')->whereNull('merged_into_id')->whereNull('deleted_at')->groupBy('email')->havingRaw('COUNT(*) > 1')->get()->count(),
'name_birthdate' => DB::table('contacts')->selectRaw('COUNT(*) as cnt')->whereNotNull('name')->whereNotNull('firstname')->whereNotNull('birthdate')->whereNull('merged_into_id')->whereNull('deleted_at')->groupBy('name', 'firstname', 'birthdate')->havingRaw('COUNT(*) > 1')->get()->count(),
'name_zip' => DB::table('contacts')->selectRaw('COUNT(*) as cnt')->whereNotNull('name')->whereNotNull('firstname')->whereNotNull('zip')->where('zip', '!=', '')->whereNull('merged_into_id')->whereNull('deleted_at')->groupBy('name', 'firstname', 'zip')->havingRaw('COUNT(*) > 1')->get()->count(),
default => 0,
};
}
private function findByEmail(): array
{
return DB::table('contacts')
->selectRaw('GROUP_CONCAT(id ORDER BY updated_at DESC, id DESC) as ids')
->whereNotNull('email')->where('email', '!=', '')
->whereNull('merged_into_id')->whereNull('deleted_at')
->groupBy('email')->havingRaw('COUNT(*) > 1')
->pluck('ids')->map(fn ($s) => array_map('intval', explode(',', $s)))->all();
}
private function findByNameBirthdate(): array
{
return DB::table('contacts')
->selectRaw('GROUP_CONCAT(id ORDER BY updated_at DESC, id DESC) as ids')
->whereNotNull('name')->whereNotNull('firstname')->whereNotNull('birthdate')
->whereNull('merged_into_id')->whereNull('deleted_at')
->groupBy('name', 'firstname', 'birthdate')->havingRaw('COUNT(*) > 1')
->pluck('ids')->map(fn ($s) => array_map('intval', explode(',', $s)))->all();
}
private function findByNameZip(): array
{
return DB::table('contacts')
->selectRaw('GROUP_CONCAT(id ORDER BY updated_at DESC, id DESC) as ids')
->whereNotNull('name')->whereNotNull('firstname')
->whereNotNull('zip')->where('zip', '!=', '')
->whereNull('merged_into_id')->whereNull('deleted_at')
->groupBy('name', 'firstname', 'zip')->havingRaw('COUNT(*) > 1')
->pluck('ids')->map(fn ($s) => array_map('intval', explode(',', $s)))->all();
}
public function restore(int $id): \Illuminate\Http\JsonResponse
{
$contact = Contact::onlyTrashed()->withoutGlobalScope('not_merged')->findOrFail($id);
$contact->restore();
return response()->json(['success' => true]);
}
public function getContacts(Request $request): \Illuminate\Http\JsonResponse
{
$showDeleted = $request->filled('filter_deleted');
// Reihenfolge wichtig: select() zuerst, dann withCount() — sonst überschreibt
// select() die COUNT-Subqueries die withCount() per addSelect() eingetragen hat.
$query = $showDeleted
? Contact::onlyTrashed()->withoutGlobalScope('not_merged')->select('contacts.*')->withCount(['leads', 'bookings'])
: Contact::select('contacts.*')->withCount(['leads', 'bookings']);
// Zusatzfilter aus der UI (werden als extra GET-Parameter gesendet)
if ($request->filled('filter_has_leads')) {
$query->has('leads');
}
if ($request->filled('filter_has_bookings')) {
$query->has('bookings');
}
if ($request->filled('filter_has_email')) {
$query->whereNotNull('email')->where('email', '!=', '');
}
return DataTables::eloquent($query)
->addColumn('action_edit', function (Contact $contact) use ($showDeleted) {
if ($showDeleted) {
return '';
}
return '<a href="' . route('contact_detail', [$contact->id]) . '" class="btn icon-btn btn-sm btn-primary" title="Öffnen"><span class="fa fa-edit"></span></a>';
})
->addColumn('action_delete', function (Contact $contact) use ($showDeleted) {
if ($showDeleted) {
return '<button class="btn icon-btn btn-xs btn-success ml-1 btn-contact-restore" '
. 'data-id="' . $contact->id . '" '
. 'data-name="' . e($contact->fullName()) . '" '
. 'title="Wiederherstellen"><span class="fa fa-undo"></span></button>';
}
return '<button class="btn icon-btn btn-xs btn-danger ml-1 btn-contact-delete" '
. 'data-id="' . $contact->id . '" '
. 'data-name="' . e($contact->fullName()) . '" '
. 'title="Löschen"><span class="fa fa-trash"></span></button>';
})
->addColumn('id', function (Contact $contact) use ($showDeleted) {
if ($showDeleted) {
return '<span data-order="' . $contact->id . '">' . $contact->id . '</span>';
}
return '<a data-order="' . $contact->id . '" href="' . route('contact_detail', [$contact->id]) . '">' . $contact->id . '</a>';
})
->addColumn('raw_id', fn(Contact $contact) => $contact->id)
->addColumn('leads_count', fn(Contact $contact) => $contact->leads_count)
->addColumn('bookings_count', fn(Contact $contact) => $contact->bookings_count)
->addColumn('deleted_at', fn(Contact $contact) => $contact->deleted_at?->format('d.m.Y H:i') ?? '')
->orderColumn('id', 'customer.id $1')
->orderColumn('deleted_at', 'customer.deleted_at $1')
->filterColumn('id', function ($query, $keyword) {
if ($keyword !== '') {
$query->where('contacts.id', 'LIKE', '%' . $keyword . '%');
}
})
->filterColumn('name', function ($query, $keyword) {
if ($keyword !== '') {
$query->where(function ($q) use ($keyword) {
$q->where('name', 'LIKE', '%' . $keyword . '%')
->orWhere('firstname', 'LIKE', '%' . $keyword . '%');
});
}
})
->filter(function ($query) use ($request) {
$location = $request->input('filter_location');
if ($location && $location !== '') {
$query->where(function ($q) use ($location) {
$q->where('zip', 'LIKE', '%' . $location . '%')
->orWhere('city', 'LIKE', '%' . $location . '%');
});
}
$search = $request->input('search.value');
if ($search && $search !== '') {
$query->where(function ($q) use ($search) {
$q->where('name', 'LIKE', '%' . $search . '%')
->orWhere('firstname', 'LIKE', '%' . $search . '%')
->orWhere('email', 'LIKE', '%' . $search . '%')
->orWhere('phone', 'LIKE', '%' . $search . '%')
->orWhere('phonemobile', 'LIKE', '%' . $search . '%');
});
}
}, true)
->rawColumns(['action_edit', 'action_delete', 'id'])
->make(true);
}
}

View file

@ -68,7 +68,7 @@ class CustomerController extends Controller
public function getCustomers()
{
$query = Customer::with('salutation')->select('customer.*');
$query = Customer::with('salutation')->select('contacts.*');
return \DataTables::eloquent($query)
->addColumn('action_edit', function (Customer $customer) {

View file

@ -14,15 +14,6 @@ use Request;
class HomeController extends Controller
{
/**
* Create a new controller instance.
*
* @return void
*/
public function __construct()
{
}
/**
* Show the application dashboard.
*

View file

@ -232,7 +232,7 @@ class LeadController extends Controller
public function getLeads()
{
$query = Lead::with('customer')->with('sf_guard_user')->with('status')->select('lead.*');
$query = Lead::with('customer')->with('sf_guard_user')->with('status')->select('inquiries.*');
return \DataTables::eloquent($query)
->addColumn('action_edit', function (Lead $lead) {
@ -296,7 +296,7 @@ class LeadController extends Controller
// $q->select('sent_at')->where('sent_at', DB::raw("(select max('sent_at') customer_mails)")); //)
})->orderBy(
LeadMail::select('sent_at')
->whereColumn('lead_id', 'lead.id')
->whereColumn('lead_id', 'inquiries.id')
->orderBy('sent_at', 'DESC')
->limit(1)
, $order);

View file

@ -89,7 +89,7 @@ class RequestController extends Controller
wirte old where has state to new has travel_documents
$bs = Booking::whereHas('arrangements', function($q){
$q->where('state', '!=', NULL);
})->where('lead_id', '!=', NULL)->where('new_drafts', 0)->get();
})->where('inquiry_id', '!=', NULL)->where('new_drafts', 0)->get();
foreach ($bs as $b){
$b->travel_documents = true;
@ -101,7 +101,7 @@ class RequestController extends Controller
private function getSearchRequests()
{
$query = Booking::with('lead')->with('customer')->with('customer_mails')->with('customer_mails')->select('booking.*')->where('lead_id', '!=', NULL);
$query = Booking::with('lead')->with('customer')->with('customer_mails')->with('customer_mails')->select('booking.*')->where('inquiry_id', '!=', NULL);
if (Request::get('full_firstname_search') != "") {
$query->whereHas('customer', function ($q) {
@ -241,7 +241,7 @@ class RequestController extends Controller
}
if (Request::get('full_lead_id_search') != "") {
$query->where('lead_id', 'LIKE', '%' . Request::get('full_lead_id_search') . '%');
$query->where('inquiry_id', 'LIKE', '%' . Request::get('full_lead_id_search') . '%');
}
if (Request::get('full_booking_id_search') != "") {
$query->where('id', 'LIKE', '%' . Request::get('full_booking_id_search') . '%');
@ -398,10 +398,10 @@ class RequestController extends Controller
return '<a data-order="' . $booking->id . '" href="' . make_old_url('booking/' . $booking->id . '/edit') . '" data-id="' . $booking->id . '">' . $booking->id . '</a>';
})
->addColumn('action_lead_edit', function (Booking $booking) {
return '<a href="' . route('lead_detail', [$booking->lead_id]) . '" class="btn icon-btn btn-sm btn-primary"><span class="fa fa-edit"></span></a>';
return '<a href="' . route('lead_detail', [$booking->inquiry_id]) . '" class="btn icon-btn btn-sm btn-primary"><span class="fa fa-edit"></span></a>';
})
->addColumn('lead_id', function (Booking $booking) {
return '<a data-order="' . $booking->lead_id . '" href="' . make_old_url('leads/' . $booking->lead_id . '/edit') . '" data-id="' . $booking->lead_id . '">' . $booking->lead_id . '</a>';
return '<a data-order="' . $booking->inquiry_id . '" href="' . make_old_url('leads/' . $booking->inquiry_id . '/edit') . '" data-id="' . $booking->inquiry_id . '">' . $booking->inquiry_id . '</a>';
})
->addColumn('travel_country_id', function (Booking $booking) {
return '<span data-order="' . ($booking->travel_country_id ? $booking->travel_country_id : 0) . '">' . ($booking->travel_country_id ? $booking->travel_country->name : "-") . '</span>';
@ -523,7 +523,7 @@ class RequestController extends Controller
}
})
*/
->orderColumn('lead_id', 'lead_id $1')
->orderColumn('lead_id', 'inquiry_id $1')
->orderColumn('id', 'id $1')
->orderColumn('travel_country_id', 'travel_country_id $1')
->orderColumn('travelagenda_id', 'travelagenda_id $1')

View file

@ -14,7 +14,7 @@ class Kernel extends HttpKernel
* @var array
*/
protected $middleware = [
\Illuminate\Foundation\Http\Middleware\CheckForMaintenanceMode::class,
\Illuminate\Foundation\Http\Middleware\PreventRequestsDuringMaintenance::class,
\Illuminate\Foundation\Http\Middleware\ValidatePostSize::class,
\App\Http\Middleware\TrimStrings::class,
\Illuminate\Foundation\Http\Middleware\ConvertEmptyStringsToNull::class,
@ -39,19 +39,17 @@ class Kernel extends HttpKernel
],
'api' => [
'throttle:60,1',
'bindings',
\Illuminate\Routing\Middleware\SubstituteBindings::class,
'throttle:api',
],
];
/**
* The application's route middleware.
* The application's route middleware aliases.
*
* These middleware may be assigned to groups or used individually.
*
* @var array
* @var array<string, class-string|string>
*/
protected $routeMiddleware = [
protected $middlewareAliases = [
'auth' => \App\Http\Middleware\Authenticate::class,
'auth.basic' => \Illuminate\Auth\Middleware\AuthenticateWithBasicAuth::class,
'2fa' => \App\Http\Middleware\MiddleGoogle2FA::class,

View file

@ -2,8 +2,8 @@
namespace App\Http\Middleware;
use Illuminate\Http\Middleware\TrustProxies as Middleware;
use Illuminate\Http\Request;
use Fideloper\Proxy\TrustProxies as Middleware;
class TrustProxies extends Middleware
{
@ -19,5 +19,11 @@ class TrustProxies extends Middleware
*
* @var int
*/
protected $headers = Request::HEADER_X_FORWARDED_ALL;
protected $headers =
Request::HEADER_X_FORWARDED_FOR |
Request::HEADER_X_FORWARDED_HOST |
Request::HEADER_X_FORWARDED_PORT |
Request::HEADER_X_FORWARDED_PROTO |
Request::HEADER_X_FORWARDED_PREFIX |
Request::HEADER_X_FORWARDED_AWS_ELB;
}

View file

@ -17,7 +17,7 @@ class CreatePDF{
{
$this->view = $view;
$this->disk = $disk;
$this->prepath = Storage::disk($disk)->getAdapter()->getPathPrefix();
$this->prepath = Storage::disk($disk)->path('');
}

View file

@ -32,7 +32,10 @@ class Account extends Model
use SoftDeletes;
protected $dates = ['deleted_at'];
protected $casts = [
'deleted_at' => 'datetime',
];
public function user()

View file

@ -55,14 +55,12 @@ class Arrangement extends Model
'view_position' => 'int',
'booking_id' => 'int',
'type_id' => 'int',
'in_pdf' => 'bool'
'in_pdf' => 'bool',
'state' => 'datetime',
'begin' => 'datetime',
'end' => 'datetime',
];
protected $dates = [
'state',
'begin',
'end'
];
protected $fillable = [
'template_id',

View file

@ -9,6 +9,7 @@ namespace App\Models;
use Carbon\Carbon;
use App\Services\Util;
use App\Services\Passolution;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Collection;
@ -18,7 +19,7 @@ use Illuminate\Database\Eloquent\Collection;
* @property int $id
* @property Carbon $booking_date
* @property int $customer_id
* @property int $lead_id
* @property int $inquiry_id
* @property bool $new_drafts
* @property int $sf_guard_user_id
* @property int $branch_id
@ -203,13 +204,16 @@ use Illuminate\Database\Eloquent\Collection;
*/
class Booking extends Model
{
use HasFactory;
protected $connection = 'mysql';
protected $table = 'booking';
protected $casts = [
'customer_id' => 'int',
'lead_id' => 'int',
'inquiry_id' => 'int',
'offer_id' => 'int',
'new_drafts' => 'bool',
'sf_guard_user_id' => 'int',
'branch_id' => 'int',
@ -237,25 +241,24 @@ class Booking extends Model
'is_rail_fly' => 'bool',
'comfort' => 'bool',
'airline_ids' => 'array',
'participant_pass' => 'bool'
];
'participant_pass' => 'bool',
'booking_date' => 'datetime',
'start_date' => 'datetime',
'end_date' => 'datetime',
'participant_birthdate' => 'datetime',
'final_payment_date' => 'datetime',
'refund_date' => 'datetime',
'lawyer_date' => 'datetime',
'xx_tkt_date' => 'datetime',
];
protected $dates = [
'booking_date',
'start_date',
'end_date',
'participant_birthdate',
'final_payment_date',
'refund_date',
'lawyer_date',
'xx_tkt_date'
];
protected $fillable = [
'booking_date',
'customer_id',
'lead_id',
'inquiry_id',
'offer_id',
'new_drafts',
'sf_guard_user_id',
'branch_id',
@ -392,9 +395,29 @@ class Booking extends Model
return $this->belongsTo(Customer::class);
}
/**
* Lead/Inquiry der Buchung.
* FK-Spalte `inquiry_id` (vormals `lead_id` Modul 3 Phase 2 Rename).
* Methodenname bleibt `lead()` für Legacy-Kompatibilität; {@see self::inquiry()}
* ist der fachlich korrekte Alias und sollte in neuem Code verwendet werden.
*/
public function lead()
{
return $this->belongsTo(Lead::class);
return $this->belongsTo(Lead::class, 'inquiry_id');
}
public function inquiry()
{
return $this->belongsTo(Lead::class, 'inquiry_id');
}
/**
* Angebot, aus dem diese Buchung entstanden ist (Modul 6, Ticket B8).
* Nullable nicht jede Buchung hat einen Angebots-Vorlauf.
*/
public function offer()
{
return $this->belongsTo(\App\Models\Offer::class);
}
public function sf_guard_user()

View file

@ -48,12 +48,10 @@ class BookingConfirmation extends Model
'total' => 'float',
'deposit' => 'float',
'final_payment' => 'float',
'deposit_payment_date' => 'datetime',
'final_payment_date' => 'datetime',
];
protected $dates = [
'deposit_payment_date',
'final_payment_date'
];
protected $fillable = [
'booking_id',

View file

@ -71,11 +71,9 @@ class BookingDocument extends Model
'status' => 'int',
'booking_storno_id' => 'int',
'coupon_id' => 'int',
'date' => 'datetime',
];
protected $dates = [
'date'
];
protected $fillable = [
'booking_id',

View file

@ -48,12 +48,10 @@ class BookingInvoice extends Model
'total' => 'float',
'deposit' => 'float',
'final_payment' => 'float',
'deposit_payment_date' => 'datetime',
'final_payment_date' => 'datetime',
];
protected $dates = [
'deposit_payment_date',
'final_payment_date'
];
protected $fillable = [
'booking_id',

View file

@ -52,12 +52,10 @@ class BookingNotice extends Model
'from_user_id' => 'int',
'to_user_id' => 'int',
'show' => 'bool',
'important' => 'bool'
'important' => 'bool',
'edit_at' => 'datetime',
];
protected $dates = [
'edit_at',
];
protected $fillable = [
'booking_id',

View file

@ -55,12 +55,10 @@ class BookingServiceItem extends Model
'service_price' => 'float',
'service_price_refund' => 'float',
'commission' => 'float',
'is_commission_locked' => 'bool'
'is_commission_locked' => 'bool',
'travel_date' => 'datetime',
];
protected $dates = [
'travel_date'
];
protected $fillable = [
'booking_id',

View file

@ -49,13 +49,11 @@ class BookingStorno extends Model
'booking_id' => 'int',
'total' => 'float',
'storno' => 'float',
'done' => 'bool'
'done' => 'bool',
'storno_date' => 'datetime',
'storno_print' => 'datetime',
];
protected $dates = [
'storno_date',
'storno_print'
];
protected $fillable = [
'booking_id',

164
app/Models/Contact.php Normal file
View file

@ -0,0 +1,164 @@
<?php
namespace App\Models;
use App\Models\Sym\TravelCountry;
use Carbon\Carbon;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Collection;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\SoftDeletes;
/**
* Kontakt-Modell saubere Neuimplementierung auf Basis der customer-Tabelle.
*
* Unterschiede zum alten Customer-Modell:
* - Global Scope schließt zusammengeführte Duplikate (merged_into_id IS NOT NULL) aus
* - merged_into_id + merged_at in $fillable
* - mergedInto() / mergedContacts() Beziehungen
*
* Tabellen-Name: 'contacts' (nach Phase 2 RENAME TABLE customer contacts).
*
* @property int $id
* @property int|null $salutation_id
* @property string|null $title
* @property string|null $name
* @property string|null $firstname
* @property Carbon|null $birthdate
* @property string|null $company
* @property string|null $street
* @property string|null $zip
* @property string|null $city
* @property string|null $email
* @property string|null $phone
* @property string|null $phonebusiness
* @property string|null $phonemobile
* @property string|null $fax
* @property int|null $merged_into_id
* @property Carbon|null $merged_at
* @property Carbon $created_at
* @property Carbon $updated_at
* @property-read Contact|null $mergedInto
* @property-read Collection|Contact[] $mergedContacts
* @property-read Collection|Lead[] $leads
* @property-read Collection|Booking[] $bookings
*/
class Contact extends Model
{
use HasFactory, SoftDeletes;
protected $connection = 'mysql';
protected $table = 'contacts';
protected $casts = [
'salutation_id' => 'int',
'credit_card_type_id' => 'int',
'country_id' => 'int',
'merged_into_id' => 'int',
'birthdate' => 'datetime',
'credit_card_expiration_date' => 'datetime',
'merged_at' => 'datetime',
];
protected $fillable = [
'salutation_id',
'title',
'name',
'firstname',
'birthdate',
'company',
'street',
'zip',
'city',
'email',
'phone',
'phonebusiness',
'phonemobile',
'fax',
'bank',
'bank_code',
'bank_account_number',
'credit_card_type_id',
'credit_card_number',
'credit_card_expiration_date',
'participants_remarks',
'miscellaneous_remarks',
'country_id',
'merged_into_id',
'merged_at',
];
/**
* Globaler Scope: zusammengeführte Duplikate werden standardmäßig ausgeblendet.
* Für Zugriff auf alle inkl. Duplikate: Contact::withoutGlobalScope('not_merged')
*/
protected static function booted(): void
{
static::addGlobalScope('not_merged', function (Builder $query) {
$query->whereNull('merged_into_id');
});
}
// ── Beziehungen ──────────────────────────────────────────────────────────
public function mergedInto(): \Illuminate\Database\Eloquent\Relations\BelongsTo
{
return $this->belongsTo(Contact::class, 'merged_into_id')
->withoutGlobalScope('not_merged');
}
public function mergedContacts(): \Illuminate\Database\Eloquent\Relations\HasMany
{
return $this->hasMany(Contact::class, 'merged_into_id')
->withoutGlobalScope('not_merged');
}
public function leads(): \Illuminate\Database\Eloquent\Relations\HasMany
{
return $this->hasMany(Lead::class, 'customer_id')->orderByDesc('created_at');
}
public function bookings(): \Illuminate\Database\Eloquent\Relations\HasMany
{
return $this->hasMany(Booking::class, 'customer_id')->orderByDesc('created_at');
}
public function salutation(): \Illuminate\Database\Eloquent\Relations\BelongsTo
{
return $this->belongsTo(Salutation::class);
}
public function travel_country(): \Illuminate\Database\Eloquent\Relations\BelongsTo
{
return $this->belongsTo(TravelCountry::class, 'country_id');
}
// ── Hilfsmethoden ────────────────────────────────────────────────────────
public function fullName(): string
{
if ($this->firstname) {
return $this->firstname . ' ' . $this->name;
}
return (string) $this->name;
}
public function isMerged(): bool
{
return $this->merged_into_id !== null;
}
public static function getCountriesArray(): \Illuminate\Support\Collection
{
return TravelCountry::where('is_customer_country', 1)->get()->pluck('name', 'id');
}
public static $salutationType = [
1 => 'Herr',
2 => 'Frau',
3 => 'Divers/keine Anrede',
4 => 'Firma',
];
}

View file

@ -59,14 +59,12 @@ class Coupon extends Model
'customer_id' => 'int',
'booking_id' => 'int',
'value' => 'float',
'is_redeemed' => 'bool'
'is_redeemed' => 'bool',
'issue_date' => 'datetime',
'valid_date' => 'datetime',
'redeem_date' => 'datetime',
];
protected $dates = [
'issue_date',
'valid_date',
'redeem_date'
];
protected $fillable = [
'number',

View file

@ -9,6 +9,7 @@ namespace App\Models;
use App\Models\Sym\TravelCountry;
use Carbon\Carbon;
use Illuminate\Database\Eloquent\Collection;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
/**
@ -83,20 +84,26 @@ use Illuminate\Database\Eloquent\Model;
*/
class Customer extends Model
{
use HasFactory;
protected $connection = 'mysql';
protected $table = 'customer';
/**
* Modul 3 Phase 2: customer contacts (RENAME TABLE).
* Der Model-Name bleibt aus Kompatibilität zum Legacy-Code bestehen;
* für die Neuimplementierung steht {@see Contact} bereit.
*/
protected $table = 'contacts';
protected $casts = [
'salutation_id' => 'int',
'credit_card_type_id' => 'int',
'country_id' => 'int'
];
'country_id' => 'int',
'birthdate' => 'datetime',
'credit_card_expiration_date' => 'datetime',
];
protected $dates = [
'birthdate',
'credit_card_expiration_date'
];
protected $fillable = [
'salutation_id',

View file

@ -60,10 +60,11 @@ class CustomerFewoFile extends Model
protected $casts = [
'travel_user_id' => 'int',
'customer_fewo_mail_id' => 'int',
'size' => 'int'
'size' => 'int',
'deleted_at' => 'datetime',
];
protected $dates = ['deleted_at'];
protected $fillable = [
'travel_user_id',

View file

@ -106,15 +106,13 @@ class CustomerFewoMail extends Model
'recipient' => 'array',
'forward' => 'array',
'cc' => 'array',
'bcc' => 'array'
'bcc' => 'array',
'sent_at' => 'datetime',
'scheduled_at' => 'datetime',
'delivered_at' => 'datetime',
'deleted_at' => 'datetime',
];
protected $dates = [
'sent_at',
'scheduled_at',
'delivered_at',
'deleted_at'
];
protected $fillable = [
'travel_user_booking_fewo_id',

View file

@ -109,14 +109,12 @@ class CustomerMail extends Model
'recipient' => 'array',
'forward' => 'array',
'cc' => 'array',
'bcc' => 'array'
'bcc' => 'array',
'sent_at' => 'datetime',
'scheduled_at' => 'datetime',
'delivered_at' => 'datetime',
];
protected $dates = [
'sent_at',
'scheduled_at',
'delivered_at'
];
protected $fillable = [
'booking_id',

View file

@ -41,13 +41,11 @@ class FewoReservation extends Model
protected $casts = [
'lodging_id' => 'int',
'status' => 'int',
'type' => 'int'
'type' => 'int',
'from_date' => 'datetime',
'to_date' => 'datetime',
];
protected $dates = [
'from_date',
'to_date'
];
protected $fillable = [
'lodging_id',

View file

@ -43,13 +43,11 @@ class FewoSeason extends Model
protected $casts = [
'minimum_stay' => 'int',
'only_weekday' => 'int'
'only_weekday' => 'int',
'from_date' => 'datetime',
'to_date' => 'datetime',
];
protected $dates = [
'from_date',
'to_date'
];
protected $fillable = [
'name',

View file

@ -17,7 +17,7 @@ use Illuminate\Database\Eloquent\Model;
* @property int $tag_id
* @property Carbon $created_at
* @property Carbon $updated_at
*
*
* //* @property IQContentFile $i_q_content_file
* @property IQContentTag $i_q_content_tag
* @package App\Models

View file

@ -57,7 +57,7 @@ class IQContentTree extends Model
protected $connection = 'mysql_stern';
protected $dates = ['deleted_at'];
protected $table = 'i_q_content_trees';
@ -65,7 +65,9 @@ class IQContentTree extends Model
'name', 'identifier', 'description', 'settings', 'pos', 'active',
];
protected $casts = ['settings' => 'array'];
protected $casts = ['settings' => 'array',
'deleted_at' => 'datetime',
];
public function sluggable(): array
{

View file

@ -72,7 +72,7 @@ class IQContentTreeNode extends Model
protected $connection = 'mysql_stern';
protected $dates = ['deleted_at'];
protected $table = 'i_q_content_tree_nodes';
@ -80,7 +80,9 @@ class IQContentTreeNode extends Model
'tree_id', 'parent_id', 'lvl', 'name', 'identifier', 'title', 'description', 'settings', 'image', 'pos', 'active',
];
protected $casts = ['settings' => 'array', 'image' => 'array'];
protected $casts = ['settings' => 'array', 'image' => 'array',
'deleted_at' => 'datetime',
];
public function sluggable(): array
{

View file

@ -53,13 +53,11 @@ class Inquiry extends Model
'template_id' => 'int',
'in_pdf' => 'bool',
'type_id' => 'int',
'view_position' => 'int'
'view_position' => 'int',
'begin' => 'datetime',
'end' => 'datetime',
];
protected $dates = [
'begin',
'end'
];
protected $fillable = [
'lead_id',

View file

@ -9,6 +9,7 @@ namespace App\Models;
use Carbon\Carbon;
use App\Services\Passolution;
use App\Models\Lead as ModelsLead;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Collection;
@ -107,9 +108,16 @@ use Illuminate\Database\Eloquent\Collection;
*/
class Lead extends Model
{
use HasFactory;
protected $connection = 'mysql';
protected $table = 'lead';
/**
* Modul 3 Phase 2: lead inquiries (RENAME TABLE).
* Model-Name bleibt (um Breaking Changes in der gesamten Codebase zu vermeiden);
* fachlich ist das Modell jetzt eine "Inquiry" (Anfrage).
*/
protected $table = 'inquiries';
protected $casts = [
'customer_id' => 'int',
@ -126,16 +134,14 @@ class Lead extends Model
'travelcategory_id' => 'int',
'price' => 'float',
'pax' => 'int',
'participant_salutation_id' => 'int'
'participant_salutation_id' => 'int',
'request_date' => 'datetime',
'travelperiod_start' => 'datetime',
'travelperiod_end' => 'datetime',
'next_due_date' => 'datetime',
'participant_birthdate' => 'datetime',
];
protected $dates = [
'request_date',
'travelperiod_start',
'travelperiod_end',
'next_due_date',
'participant_birthdate'
];
protected $fillable = [
'customer_id',

View file

@ -91,14 +91,12 @@ class LeadMail extends Model
'recipient' => 'array',
'forward' => 'array',
'cc' => 'array',
'bcc' => 'array'
'bcc' => 'array',
'sent_at' => 'datetime',
'scheduled_at' => 'datetime',
'delivered_at' => 'datetime',
];
protected $dates = [
'sent_at',
'scheduled_at',
'delivered_at'
];
protected $fillable = [
'lead_id',

View file

@ -53,12 +53,10 @@ class LeadNotice extends Model
'from_user_id' => 'int',
'to_user_id' => 'int',
'show' => 'bool',
'important' => 'bool'
'important' => 'bool',
'edit_at' => 'datetime',
];
protected $dates = [
'edit_at'
];
protected $fillable = [
'lead_id',

View file

@ -46,12 +46,10 @@ class LeadParticipant extends Model
protected $casts = [
'lead_id' => 'int',
'participant_salutation_id' => 'int'
'participant_salutation_id' => 'int',
'participant_birthdate' => 'datetime',
];
protected $dates = [
'participant_birthdate'
];
protected $fillable = [
'lead_id',

View file

@ -36,6 +36,49 @@ use Illuminate\Database\Eloquent\Collection;
* @property-read TravelUser|null $travel_user
* @property-read Collection|NewsletterLog[] $logs
* @package App\Models
* @property-read mixed $full_name
* @property-read mixed $groups
* @property-read mixed $groups_string
* @property-read mixed $source_label
* @property-read mixed $status_badge
* @property-read mixed $status_color
* @property-read mixed $status_label
* @property-read mixed $total_bookings
* @property-read int|null $logs_count
* @method static \Illuminate\Database\Eloquent\Builder|NewsletterContact active()
* @method static \Illuminate\Database\Eloquent\Builder|NewsletterContact ferienwohnungen()
* @method static \Illuminate\Database\Eloquent\Builder|NewsletterContact kulturreisen()
* @method static \Illuminate\Database\Eloquent\Builder|NewsletterContact multipleBookers()
* @method static \Illuminate\Database\Eloquent\Builder|NewsletterContact newModelQuery()
* @method static \Illuminate\Database\Eloquent\Builder|NewsletterContact newQuery()
* @method static \Illuminate\Database\Eloquent\Builder|NewsletterContact onlyTrashed()
* @method static \Illuminate\Database\Eloquent\Builder|NewsletterContact query()
* @method static \Illuminate\Database\Eloquent\Builder|NewsletterContact whereCreatedAt($value)
* @method static \Illuminate\Database\Eloquent\Builder|NewsletterContact whereCustomerId($value)
* @method static \Illuminate\Database\Eloquent\Builder|NewsletterContact whereDeletedAt($value)
* @method static \Illuminate\Database\Eloquent\Builder|NewsletterContact whereEmail($value)
* @method static \Illuminate\Database\Eloquent\Builder|NewsletterContact whereFirstname($value)
* @method static \Illuminate\Database\Eloquent\Builder|NewsletterContact whereGroupFerienwohnungen($value)
* @method static \Illuminate\Database\Eloquent\Builder|NewsletterContact whereGroupKulturreisen($value)
* @method static \Illuminate\Database\Eloquent\Builder|NewsletterContact whereId($value)
* @method static \Illuminate\Database\Eloquent\Builder|NewsletterContact whereLastBookingAt($value)
* @method static \Illuminate\Database\Eloquent\Builder|NewsletterContact whereLastSyncedAt($value)
* @method static \Illuminate\Database\Eloquent\Builder|NewsletterContact whereLastTravelEndDate($value)
* @method static \Illuminate\Database\Eloquent\Builder|NewsletterContact whereLastname($value)
* @method static \Illuminate\Database\Eloquent\Builder|NewsletterContact whereNotes($value)
* @method static \Illuminate\Database\Eloquent\Builder|NewsletterContact whereSource($value)
* @method static \Illuminate\Database\Eloquent\Builder|NewsletterContact whereStatus($value)
* @method static \Illuminate\Database\Eloquent\Builder|NewsletterContact whereSubscribedAt($value)
* @method static \Illuminate\Database\Eloquent\Builder|NewsletterContact whereSyncHash($value)
* @method static \Illuminate\Database\Eloquent\Builder|NewsletterContact whereTotalBookingsFerienwohnungen($value)
* @method static \Illuminate\Database\Eloquent\Builder|NewsletterContact whereTotalBookingsKulturreisen($value)
* @method static \Illuminate\Database\Eloquent\Builder|NewsletterContact whereTravelUserId($value)
* @method static \Illuminate\Database\Eloquent\Builder|NewsletterContact whereUnsubscribedAt($value)
* @method static \Illuminate\Database\Eloquent\Builder|NewsletterContact whereUpdatedAt($value)
* @method static \Illuminate\Database\Eloquent\Builder|NewsletterContact withBookings()
* @method static \Illuminate\Database\Eloquent\Builder|NewsletterContact withTrashed()
* @method static \Illuminate\Database\Eloquent\Builder|NewsletterContact withoutTrashed()
* @mixin \Eloquent
*/
class NewsletterContact extends Model
{

View file

@ -19,6 +19,19 @@ use Illuminate\Database\Eloquent\Model;
* @property-read NewsletterContact $newsletter_contact
* @property-read SfGuardUser|null $user
* @package App\Models
* @property-read mixed $action_label
* @method static \Illuminate\Database\Eloquent\Builder|NewsletterLog newModelQuery()
* @method static \Illuminate\Database\Eloquent\Builder|NewsletterLog newQuery()
* @method static \Illuminate\Database\Eloquent\Builder|NewsletterLog query()
* @method static \Illuminate\Database\Eloquent\Builder|NewsletterLog whereAction($value)
* @method static \Illuminate\Database\Eloquent\Builder|NewsletterLog whereCreatedAt($value)
* @method static \Illuminate\Database\Eloquent\Builder|NewsletterLog whereDescription($value)
* @method static \Illuminate\Database\Eloquent\Builder|NewsletterLog whereId($value)
* @method static \Illuminate\Database\Eloquent\Builder|NewsletterLog whereMetadata($value)
* @method static \Illuminate\Database\Eloquent\Builder|NewsletterLog whereNewsletterContactId($value)
* @method static \Illuminate\Database\Eloquent\Builder|NewsletterLog whereUpdatedAt($value)
* @method static \Illuminate\Database\Eloquent\Builder|NewsletterLog whereUserId($value)
* @mixin \Eloquent
*/
class NewsletterLog extends Model
{

View file

@ -1,56 +1,132 @@
<?php
/**
* Created by Reliese Model.
*/
namespace App\Models;
use App\User;
use Carbon\Carbon;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Collection;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\HasMany;
use Illuminate\Database\Eloquent\SoftDeletes;
/**
* Class Offer
* Angebot (Modul 6).
*
* Ein Offer ist der logische Angebots-Kopf (Angebotsnummer, Status,
* Referenzen). Die Inhalte (Texte, Positionen, PDF) liegen versionsweise
* in {@see OfferVersion}. Nach dem ersten Versand ist jede Änderung
* eine neue Version (Entscheidung 17.1 Entwicklungsplan).
*
* @property int $id
* @property int $lead_id
* @property float $total
* @property boolean $binary_data
* @property string $offer_number
* @property int $contact_id
* @property int|null $inquiry_id
* @property int|null $booking_id
* @property string $status
* @property int|null $current_version_id
* @property int $created_by
* @property Carbon $created_at
* @property Carbon $updated_at
* @property Lead $lead
* @package App\Models
* @method static \Illuminate\Database\Eloquent\Builder|\App\Models\Offer newModelQuery()
* @method static \Illuminate\Database\Eloquent\Builder|\App\Models\Offer newQuery()
* @method static \Illuminate\Database\Eloquent\Builder|\App\Models\Offer query()
* @method static \Illuminate\Database\Eloquent\Builder|\App\Models\Offer whereBinaryData($value)
* @method static \Illuminate\Database\Eloquent\Builder|\App\Models\Offer whereCreatedAt($value)
* @method static \Illuminate\Database\Eloquent\Builder|\App\Models\Offer whereId($value)
* @method static \Illuminate\Database\Eloquent\Builder|\App\Models\Offer whereLeadId($value)
* @method static \Illuminate\Database\Eloquent\Builder|\App\Models\Offer whereTotal($value)
* @method static \Illuminate\Database\Eloquent\Builder|\App\Models\Offer whereUpdatedAt($value)
* @mixin \Eloquent
* @property Carbon|null $deleted_at
* @property-read Contact $contact
* @property-read Lead|null $inquiry
* @property-read Booking|null $booking
* @property-read OfferVersion|null $currentVersion
* @property-read Collection|OfferVersion[] $versions
* @property-read Collection|OfferAccessToken[] $accessTokens
* @property-read User $creator
*/
class Offer extends Model
{
protected $connection = 'mysql';
use HasFactory, SoftDeletes;
protected $table = 'offer';
public const STATUS_DRAFT = 'draft';
public const STATUS_SENT = 'sent';
public const STATUS_ACCEPTED = 'accepted';
public const STATUS_DECLINED = 'declined';
public const STATUS_EXPIRED = 'expired';
public const STATUS_WITHDRAWN = 'withdrawn';
protected $casts = [
'lead_id' => 'int',
'total' => 'float',
'binary_data' => 'boolean'
];
public const STATUSES = [
self::STATUS_DRAFT,
self::STATUS_SENT,
self::STATUS_ACCEPTED,
self::STATUS_DECLINED,
self::STATUS_EXPIRED,
self::STATUS_WITHDRAWN,
];
protected $fillable = [
'lead_id',
'total',
'binary_data'
];
protected $table = 'offers';
public function lead()
{
return $this->belongsTo(Lead::class);
}
protected $fillable = [
'offer_number',
'contact_id',
'inquiry_id',
'booking_id',
'status',
'current_version_id',
'created_by',
];
protected $casts = [
'contact_id' => 'int',
'inquiry_id' => 'int',
'booking_id' => 'int',
'current_version_id' => 'int',
'created_by' => 'int',
];
public function contact(): BelongsTo
{
return $this->belongsTo(Contact::class);
}
public function inquiry(): BelongsTo
{
// Nach Modul 3 Phase 2: `Lead`-Model bildet die `inquiries`-Tabelle ab
return $this->belongsTo(Lead::class, 'inquiry_id');
}
public function booking(): BelongsTo
{
return $this->belongsTo(Booking::class);
}
public function currentVersion(): BelongsTo
{
return $this->belongsTo(OfferVersion::class, 'current_version_id');
}
public function versions(): HasMany
{
return $this->hasMany(OfferVersion::class)->orderBy('version_no');
}
public function accessTokens(): HasMany
{
return $this->hasMany(OfferAccessToken::class);
}
public function creator(): BelongsTo
{
return $this->belongsTo(User::class, 'created_by');
}
public function scopeStatus(Builder $q, string $status): Builder
{
return $q->where('status', $status);
}
public function scopeOpen(Builder $q): Builder
{
return $q->whereIn('status', [self::STATUS_DRAFT, self::STATUS_SENT]);
}
public function isEditable(): bool
{
return in_array($this->status, [self::STATUS_DRAFT, self::STATUS_SENT], true);
}
}

View file

@ -0,0 +1,120 @@
<?php
namespace App\Models;
use Carbon\Carbon;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Support\Str;
/**
* Kundenseitiger Zugriffstoken für /angebot/{token} (Modul 6 / Phase D).
*
* In der Datenbank wird ausschließlich der SHA-256-Hash des Klartext-
* Tokens gespeichert. Der Klartext wird einmalig bei der Erzeugung
* zurückgegeben (siehe {@see self::generate()}) und an den Kunden
* per Mail-Link ausgeliefert.
*
* Pro Angebot + Version existiert genau ein aktiver Token; wird eine
* neue Version versendet, setzt der OfferService den Vorgänger auf
* `revoked_at`.
*
* @property int $id
* @property int $offer_id
* @property int $offer_version_id
* @property string $token_hash
* @property Carbon|null $expires_at
* @property Carbon|null $first_opened_at
* @property Carbon|null $revoked_at
* @property Carbon $created_at
* @property Carbon $updated_at
* @property-read Offer $offer
* @property-read OfferVersion $version
*/
class OfferAccessToken extends Model
{
use HasFactory;
protected $table = 'offer_access_tokens';
protected $fillable = [
'offer_id',
'offer_version_id',
'token_hash',
'expires_at',
'first_opened_at',
'revoked_at',
];
protected $casts = [
'offer_id' => 'int',
'offer_version_id' => 'int',
'expires_at' => 'datetime',
'first_opened_at' => 'datetime',
'revoked_at' => 'datetime',
];
public function offer(): BelongsTo
{
return $this->belongsTo(Offer::class);
}
public function version(): BelongsTo
{
return $this->belongsTo(OfferVersion::class, 'offer_version_id');
}
public function scopeActive(Builder $q): Builder
{
return $q->whereNull('revoked_at')
->where(function (Builder $q) {
$q->whereNull('expires_at')->orWhere('expires_at', '>', now());
});
}
/**
* Erzeugt ein neues Token für die angegebene Version und liefert
* den Klartext-Token zurück (nur einmalig abrufbar). In der
* Datenbank wird nur der Hash persistiert.
*/
public static function generate(
Offer $offer,
OfferVersion $version,
?Carbon $expiresAt = null
): array {
$plain = Str::random(48);
$hash = hash('sha256', $plain);
/** @var self $token */
$token = self::create([
'offer_id' => $offer->id,
'offer_version_id' => $version->id,
'token_hash' => $hash,
'expires_at' => $expiresAt,
]);
return ['plain' => $plain, 'token' => $token];
}
/**
* Lookup per Klartext-Token (konstantzeitig via DB-Unique-Index).
*/
public static function findByPlainToken(string $plain): ?self
{
return self::where('token_hash', hash('sha256', $plain))->first();
}
public function isActive(): bool
{
if ($this->revoked_at !== null) {
return false;
}
if ($this->expires_at !== null && $this->expires_at->isPast()) {
return false;
}
return true;
}
}

99
app/Models/OfferFile.php Normal file
View file

@ -0,0 +1,99 @@
<?php
namespace App\Models;
use Carbon\Carbon;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
/**
* Datei-Anhang einer Angebotsversion (Modul 6).
*
* Struktur ist bewusst an {@see BookingFile} angelehnt (identifier,
* filename, dir, original_name, ext, mine, size), damit der vorhandene
* `FileRepository::store()` 1:1 wiederverwendet werden kann. `mine`
* bleibt so geschrieben (statt `mime`) zur Konsistenz mit der
* booking_files-Konvention.
*
* @property int $id
* @property int $offer_version_id
* @property string|null $identifier
* @property string $filename
* @property string $dir
* @property string $original_name
* @property string $ext
* @property string $mine
* @property int $size
* @property bool $include_in_pdf
* @property Carbon $created_at
* @property Carbon $updated_at
* @property-read OfferVersion $version
*/
class OfferFile extends Model
{
use HasFactory;
protected $table = 'offer_files';
protected $fillable = [
'offer_version_id',
'identifier',
'filename',
'dir',
'original_name',
'ext',
'mine',
'size',
'include_in_pdf',
];
protected $casts = [
'offer_version_id' => 'int',
'size' => 'int',
'include_in_pdf' => 'bool',
];
public static array $iconExt = [
'default' => 'fa fa-file',
'pdf' => 'fa fa-file-pdf',
'jpg' => 'fa fa-file-image',
'jpeg' => 'fa fa-file-image',
'png' => 'fa fa-file-image',
'doc' => 'fa fa-file-word',
'docx' => 'fa fa-file-word',
];
public function version(): BelongsTo
{
return $this->belongsTo(OfferVersion::class, 'offer_version_id');
}
public function getIconExt(): string
{
return self::$iconExt[$this->ext] ?? self::$iconExt['default'];
}
public function getURL(bool|string $do = false): string
{
return route('storage_file', [$this->id, 'offer', $do]);
}
public function getPath(): string
{
return \Storage::disk('offer')->path($this->dir . $this->filename);
}
public function formatBytes(int $precision = 2): string
{
$size = $this->size;
if ($size <= 0) {
return (string) $size;
}
$base = log($size) / log(1024);
$suffixes = [' bytes', ' KB', ' MB', ' GB', ' TB'];
return round(1024 ** ($base - floor($base)), $precision) . $suffixes[floor($base)];
}
}

86
app/Models/OfferItem.php Normal file
View file

@ -0,0 +1,86 @@
<?php
namespace App\Models;
use Carbon\Carbon;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
/**
* Position einer Angebotsversion (Modul 6).
*
* `travel_program_id` und `fewo_lodging_id` bleiben FK-frei, solange
* Modul 12 (v2-Reiseverwaltung) noch nicht nach Laravel migriert ist.
* `metadata` enthält einen Snapshot der Referenzdaten (Titel, Preis,
* Leistungen) so bleiben Positionen lesbar, auch wenn das Original
* später gelöscht / migriert / umbenannt wird.
*
* @property int $id
* @property int $offer_version_id
* @property int $position
* @property string $type
* @property string $title
* @property string|null $description
* @property int $quantity
* @property float $price_per_unit
* @property float $total_price
* @property int|null $travel_program_id
* @property int|null $fewo_lodging_id
* @property array|null $metadata
* @property Carbon $created_at
* @property Carbon $updated_at
* @property-read OfferVersion $version
*/
class OfferItem extends Model
{
use HasFactory;
public const TYPE_TRAVEL = 'travel';
public const TYPE_SERVICE = 'service';
public const TYPE_OPTION = 'option';
public const TYPE_DISCOUNT = 'discount';
public const TYPE_INSURANCE = 'insurance';
public const TYPE_CUSTOM = 'custom';
protected $table = 'offer_items';
protected $fillable = [
'offer_version_id',
'position',
'type',
'title',
'description',
'quantity',
'price_per_unit',
'total_price',
'travel_program_id',
'fewo_lodging_id',
'metadata',
];
protected $casts = [
'offer_version_id' => 'int',
'position' => 'int',
'quantity' => 'int',
'price_per_unit' => 'decimal:2',
'total_price' => 'decimal:2',
'travel_program_id' => 'int',
'fewo_lodging_id' => 'int',
'metadata' => 'array',
];
public function version(): BelongsTo
{
return $this->belongsTo(OfferVersion::class, 'offer_version_id');
}
/**
* Aus Menge × Einzelpreis den Positions-Gesamtpreis berechnen
* (Rabatte negativ gehört in den Service-Layer zur Summierung).
*/
public function calculateTotal(): float
{
return round($this->quantity * (float) $this->price_per_unit, 2);
}
}

View file

@ -0,0 +1,77 @@
<?php
namespace App\Models;
use App\User;
use Carbon\Carbon;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\SoftDeletes;
/**
* Wiederverwendbare Angebots-Vorlage (Modul 6).
*
* Liefert Default-Texte + Default-Positionen für neue Angebote.
* `default_items` ist ein JSON-Array von Positionen im Schema
* [{title, description, type, price_per_unit, quantity}, ].
*
* @property int $id
* @property int|null $branch_id
* @property string $name
* @property string|null $description
* @property string|null $default_headline
* @property string|null $default_intro
* @property string|null $default_itinerary
* @property string|null $default_closing
* @property array|null $default_items
* @property bool $is_active
* @property int $created_by
* @property Carbon $created_at
* @property Carbon $updated_at
* @property Carbon|null $deleted_at
* @property-read Branch|null $branch
* @property-read User $creator
*/
class OfferTemplate extends Model
{
use HasFactory, SoftDeletes;
protected $table = 'offer_templates';
protected $fillable = [
'branch_id',
'name',
'description',
'default_headline',
'default_intro',
'default_itinerary',
'default_closing',
'default_items',
'is_active',
'created_by',
];
protected $casts = [
'branch_id' => 'int',
'default_items' => 'array',
'is_active' => 'bool',
'created_by' => 'int',
];
public function branch(): BelongsTo
{
return $this->belongsTo(Branch::class);
}
public function creator(): BelongsTo
{
return $this->belongsTo(User::class, 'created_by');
}
public function scopeActive(Builder $q): Builder
{
return $q->where('is_active', true);
}
}

126
app/Models/OfferVersion.php Normal file
View file

@ -0,0 +1,126 @@
<?php
namespace App\Models;
use App\User;
use Carbon\Carbon;
use Illuminate\Database\Eloquent\Collection;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\HasMany;
/**
* Version eines Angebots (Modul 6).
*
* Jede versendete Fassung wird hier festgehalten Texte, Positionen
* und PDF bleiben damit unveränderlich, sobald ein Kunde sie per
* Freigabe-Link einsehen kann. Neue Änderungen nach dem Versand
* erzeugen eine neue Version (version_no = max+1, status = draft).
*
* @property int $id
* @property int $offer_id
* @property int $version_no
* @property string $status
* @property Carbon|null $valid_until
* @property float $total_price
* @property string|null $headline
* @property string|null $intro_text
* @property string|null $itinerary_text
* @property string|null $closing_text
* @property int|null $template_id
* @property string|null $pdf_path
* @property bool $pdf_archived
* @property Carbon|null $sent_at
* @property Carbon|null $accepted_at
* @property string|null $accepted_via
* @property array|null $template_document_ids
* @property int $created_by
* @property Carbon $created_at
* @property Carbon $updated_at
* @property-read Offer $offer
* @property-read OfferTemplate|null $template
* @property-read Collection|OfferItem[] $items
* @property-read Collection|OfferFile[] $files
* @property-read User $creator
*/
class OfferVersion extends Model
{
use HasFactory;
public const STATUS_DRAFT = 'draft';
public const STATUS_SENT = 'sent';
public const STATUS_ACCEPTED = 'accepted';
public const STATUS_DECLINED = 'declined';
public const STATUS_EXPIRED = 'expired';
public const STATUS_SUPERSEDED = 'superseded';
public const ACCEPTED_VIA_LINK = 'customer_link';
public const ACCEPTED_VIA_ADMIN = 'admin';
public const ACCEPTED_VIA_MAIL = 'email';
protected $table = 'offer_versions';
protected $fillable = [
'offer_id',
'version_no',
'status',
'valid_until',
'total_price',
'headline',
'intro_text',
'itinerary_text',
'closing_text',
'template_id',
'pdf_path',
'pdf_archived',
'sent_at',
'accepted_at',
'accepted_via',
'template_document_ids',
'created_by',
];
protected $casts = [
'offer_id' => 'int',
'version_no' => 'int',
'valid_until' => 'date',
'total_price' => 'decimal:2',
'template_id' => 'int',
'pdf_archived' => 'bool',
'sent_at' => 'datetime',
'accepted_at' => 'datetime',
'template_document_ids' => 'array',
'created_by' => 'int',
];
public function offer(): BelongsTo
{
return $this->belongsTo(Offer::class);
}
public function template(): BelongsTo
{
return $this->belongsTo(OfferTemplate::class, 'template_id');
}
public function items(): HasMany
{
return $this->hasMany(OfferItem::class)->orderBy('position');
}
public function files(): HasMany
{
return $this->hasMany(OfferFile::class);
}
public function creator(): BelongsTo
{
return $this->belongsTo(User::class, 'created_by');
}
public function isEditable(): bool
{
return $this->status === self::STATUS_DRAFT;
}
}

View file

@ -152,12 +152,10 @@ class Page extends Model
'tree_root' => 'int',
'parent_id' => 'int',
'travel_guide_content_id' => 'int',
'fewo_lodging' => 'int'
'fewo_lodging' => 'int',
'date' => 'datetime',
];
protected $dates = [
'date'
];
protected $fillable = [
'owner',

View file

@ -53,13 +53,10 @@ class Participant extends Model
'participant_salutation_id' => 'int',
'participant_child' => 'bool',
'participant_pass' => 'bool',
'participant_storno' => 'bool'
'participant_storno' => 'bool',
'participant_birthdate' => 'datetime',
];
protected $dates = [
'participant_birthdate'
];
protected $fillable = [
'booking_id',

View file

@ -55,12 +55,10 @@ class ServiceProviderEntry extends Model
'amount' => 'float',
'amount_eur' => 'float',
'factor' => 'float',
'is_cleared' => 'bool'
'is_cleared' => 'bool',
'payment_date' => 'datetime',
];
protected $dates = [
'payment_date'
];
protected $fillable = [
'booking_id',

View file

@ -47,14 +47,12 @@ class StatusHistory extends Model
protected $casts = [
'status_id' => 'int',
'lead_id' => 'int',
'sf_guard_user_id' => 'int'
'sf_guard_user_id' => 'int',
'date' => 'datetime',
'target_date' => 'datetime',
'created_at' => 'datetime',
];
protected $dates = [
'date',
'target_date',
'created_at'
];
protected $fillable = [
'status_id',

View file

@ -137,15 +137,13 @@ class TravelBooking extends Model
'options' => 'array',
'class_options' => 'array',
'extra_category' => 'array',
'insurances' => 'array'
'insurances' => 'array',
'created' => 'datetime',
'selected_start_date' => 'datetime',
'selected_end_date' => 'datetime',
'final_payment_date' => 'datetime',
];
protected $dates = [
'created',
'selected_start_date',
'selected_end_date',
'final_payment_date'
];
protected $fillable = [
'crm_booking_id',

View file

@ -79,12 +79,10 @@ class TravelUser extends Model
protected $casts = [
'salutation_id' => 'int',
'travel_nationality_id' => 'int',
'last_user_data' => 'array'
'last_user_data' => 'array',
'birthday' => 'datetime',
];
protected $dates = [
'birthday'
];
protected $hidden = [
'password'

View file

@ -159,14 +159,12 @@ class TravelUserBookingFewo extends Model
'send_service_mail' => 'array',
'send_info_mail' => 'array',
'send_employee_mail' => 'array',
'booking_date' => 'datetime',
'from_date' => 'datetime',
'to_date' => 'datetime',
'deleted_at' => 'datetime',
];
protected $dates = [
'booking_date',
'from_date',
'to_date',
'deleted_at'
];
protected $fillable = [
'travel_user_id',
@ -545,7 +543,7 @@ class TravelUserBookingFewo extends Model
if(!Storage::disk('fewo_invoices')->exists( $dir )){
Storage::disk('fewo_invoices')->makeDirectory($dir); //creates directory
}
$path = Storage::disk('fewo_invoices')->getAdapter()->getPathPrefix();
$path = Storage::disk('fewo_invoices')->path('');
return $path.$dir;
}
@ -621,7 +619,7 @@ class TravelUserBookingFewo extends Model
if(!Storage::disk('fewo_infos')->exists( $dir )){
Storage::disk('fewo_infos')->makeDirectory($dir); //creates directory
}
$path = Storage::disk('fewo_infos')->getAdapter()->getPathPrefix();
$path = Storage::disk('fewo_infos')->path('');
return $path.$dir;
}

View file

@ -53,12 +53,10 @@ class TravelUserBookingFewoNotice extends Model
'from_user_id' => 'int',
'to_user_id' => 'int',
'show' => 'bool',
'important' => 'bool'
'important' => 'bool',
'edit_at' => 'datetime',
];
protected $dates = [
'edit_at'
];
protected $fillable = [
'travel_user_booking_fewo_id',

View file

@ -2,9 +2,8 @@
namespace App\Providers;
use Illuminate\Support\Facades\Gate;
use Illuminate\Foundation\Support\Providers\AuthServiceProvider as ServiceProvider;
use Laravel\Passport\Passport;
use Illuminate\Support\Facades\Gate;
class AuthServiceProvider extends ServiceProvider
{
@ -22,12 +21,8 @@ class AuthServiceProvider extends ServiceProvider
*
* @return void
*/
public function boot()
public function boot(): void
{
$this->registerPolicies();
Passport::routes();
//
}
}

View file

@ -2,72 +2,47 @@
namespace App\Providers;
use Illuminate\Support\Facades\Route;
use Illuminate\Cache\RateLimiting\Limit;
use Illuminate\Foundation\Support\Providers\RouteServiceProvider as ServiceProvider;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\RateLimiter;
use Illuminate\Support\Facades\Route;
class RouteServiceProvider extends ServiceProvider
{
/**
* This namespace is applied to your controller routes.
*
* In addition, it is set as the URL generator's root namespace.
* The path to your application's "home" route.
*
* @var string
*/
public const HOME = '/home';
/**
* The controller namespace for the application.
* Kept for backwards compatibility with string-based route definitions.
*
* @var string|null
*/
protected $namespace = 'App\Http\Controllers';
/**
* Define your route model bindings, pattern filters, etc.
*
* @return void
* Define your route model bindings, pattern filters, and other route configuration.
*/
public function boot()
public function boot(): void
{
//
RateLimiter::for('api', function (Request $request) {
return Limit::perMinute(60)->by($request->user()?->id ?: $request->ip());
});
parent::boot();
}
$this->routes(function () {
Route::middleware('api')
->prefix('api')
->namespace($this->namespace)
->group(base_path('routes/api.php'));
/**
* Define the routes for the application.
*
* @return void
*/
public function map()
{
$this->mapApiRoutes();
$this->mapWebRoutes();
//
}
/**
* Define the "web" routes for the application.
*
* These routes all receive session state, CSRF protection, etc.
*
* @return void
*/
protected function mapWebRoutes()
{
Route::middleware('web')
->namespace($this->namespace)
->group(base_path('routes/web.php'));
}
/**
* Define the "api" routes for the application.
*
* These routes are typically stateless.
*
* @return void
*/
protected function mapApiRoutes()
{
Route::prefix('api')
->middleware('api')
->namespace($this->namespace)
->group(base_path('routes/api.php'));
Route::middleware('web')
->namespace($this->namespace)
->group(base_path('routes/web.php'));
});
}
}

View file

@ -22,7 +22,7 @@ class BookingPDFRepository extends BaseRepository
public function __construct(Booking $model)
{
$this->model = $model;
$this->prepath = Storage::disk('public')->getAdapter()->getPathPrefix();
$this->prepath = Storage::disk('public')->path('');
}
public function update($data)
@ -63,13 +63,13 @@ class BookingPDFRepository extends BaseRepository
{
$document = new stdClass();
$document->name = 'registration';
$document->number = $this->model->lead_id;
$document->number = $this->model->inquiry_id;
$document->title = 'BUCHUNGSAUFTRAG';
$document->voucher = null;
$document->date = now();
$document->total = $this->model->getPriceRaw();
$dir = $this->getDirPath('pdf', 'booking', $document->date->format('Y'));
$filename = "Buchnungsauftrag-" . $this->model->lead_id . ".pdf";
$filename = "Buchnungsauftrag-" . $this->model->inquiry_id . ".pdf";
$pdf_file = new CreatePDF('pdf.booking_registration');
$data = [
'booking' => $this->model,
@ -85,7 +85,7 @@ class BookingPDFRepository extends BaseRepository
{
$document = new stdClass();
$document->name = 'confirmation';
$document->number = $this->model->lead_id;
$document->number = $this->model->inquiry_id;
$document->title = 'REISEBESTÄTIGUNG';
$document->voucher = null;
$document->date = now();
@ -104,7 +104,7 @@ class BookingPDFRepository extends BaseRepository
$document->final_payment_date = date('Y-m-d');
}
$dir = $this->getDirPath('pdf', 'booking', $document->date->format('Y'));
$filename = "Reisebestätigung-" . $this->model->lead_id . ".pdf";
$filename = "Reisebestätigung-" . $this->model->inquiry_id . ".pdf";
$pdf_file = new CreatePDF('pdf.booking_confirmation');
$data = [
@ -160,14 +160,14 @@ class BookingPDFRepository extends BaseRepository
{
$document = new stdClass();
$document->name = 'voucher';
$document->number = $this->model->lead_id;
$document->number = $this->model->inquiry_id;
$document->name = 'voucher';
$document->title = $agency ? 'VOUCHER Agentur' : 'VOUCHER';
$document->voucher = $agency ? 'agency' : 'client';
$document->date = now();
$dir = $this->getDirPath('pdf', 'voucher', $document->date->format('Y'));
$filename = ($agency ? 'VoucherAgentur' : 'Voucher') . "-" . $this->model->lead_id . ".pdf";
$filename = ($agency ? 'VoucherAgentur' : 'Voucher') . "-" . $this->model->inquiry_id . ".pdf";
$pdf_file = new CreatePDF('pdf.booking_voucher');
$data = [
@ -224,7 +224,7 @@ class BookingPDFRepository extends BaseRepository
//init document
$document = new stdClass();
$document->name = $identifier;
$document->number = $this->model->lead_id;
$document->number = $this->model->inquiry_id;
$document->title = 'STORNOBESTÄTIGUNG';
$document->voucher = null;
$document->date = Carbon::parse($data['storno_print']);
@ -253,7 +253,7 @@ class BookingPDFRepository extends BaseRepository
$dir = $this->getDirPath('pdf', 'storno', $document->date->format('Y'));
$filename = "Reisestornierung -" . $this->model->lead_id . ".pdf";
$filename = "Reisestornierung -" . $this->model->inquiry_id . ".pdf";
$pdf_file = new CreatePDF('pdf.booking_storno');
$data = [
@ -288,7 +288,9 @@ class BookingPDFRepository extends BaseRepository
$fill = [
'booking_id' => $this->model->id,
'customer_id' => $this->model->customer_id,
'lead_id' => $this->model->lead_id,
// booking_documents.lead_id ist ein Shadow-Feld von booking.inquiry_id;
// die Spalte selbst wird von Phase 2 nicht umbenannt.
'lead_id' => $this->model->inquiry_id,
'identifier' => $identifier,
'filename' => $filename,
'dir' => $dir,

View file

@ -0,0 +1,28 @@
<?php
namespace App\Repositories;
use App\Models\Contact;
class ContactRepository extends BaseRepository
{
public function __construct(Contact $model)
{
$this->model = $model;
}
public function updateContact(int|string $id, array $data): Contact
{
/** @var Contact $contact */
$contact = Contact::findOrFail($id);
$contact->fill($data);
$contact->save();
return $contact;
}
public function createContact(array $data): Contact
{
return Contact::create($data);
}
}

View file

@ -135,7 +135,8 @@ class CustomerMailRepository extends BaseRepository {
$customer_mail->fill([
'booking_id' => $booking->id,
'customer_id' => $booking->customer_id,
'lead_id' => $booking->lead_id,
// customer_mails.lead_id-Spalte bleibt unverändert; Wert kommt aus booking.inquiry_id
'lead_id' => $booking->inquiry_id,
'is_answer' => $is_answer,
'reply_id' => $reply_id,
'email' => $mail_from,
@ -153,7 +154,8 @@ class CustomerMailRepository extends BaseRepository {
$customer_mail = CustomerMail::create([
'booking_id' => $booking->id,
'customer_id' => $booking->customer_id,
'lead_id' => $booking->lead_id,
// customer_mails.lead_id-Spalte bleibt unverändert; Wert kommt aus booking.inquiry_id
'lead_id' => $booking->inquiry_id,
'is_answer' => $is_answer,
'reply_id' => $reply_id,
'email' => $mail_from,
@ -300,7 +302,7 @@ class CustomerMailRepository extends BaseRepository {
$value->id = $customer_mail->booking_id;
$value->booking = $booking;
$value->show = 'single';
$value->lead_title_id = " - (".$value->booking->lead_id.")";
$value->lead_title_id = " - (".$value->booking->inquiry_id.")";
$tmp = [];
@ -342,7 +344,7 @@ class CustomerMailRepository extends BaseRepository {
$value->booking = $booking;
$value->show = 'single';
$value->draft = true;
$value->lead_title_id = " - (".$value->booking->lead_id.")";
$value->lead_title_id = " - (".$value->booking->inquiry_id.")";
}else{
//multi
@ -379,8 +381,8 @@ class CustomerMailRepository extends BaseRepository {
$value->draft = false;
$value->booking = $booking;
$value->message = "";
$value->subject = " - (".$value->booking->lead_id.")";
$value->lead_title_id = " - (".$value->booking->lead_id.")";
$value->subject = " - (".$value->booking->inquiry_id.")";
$value->lead_title_id = " - (".$value->booking->inquiry_id.")";
$value->s_placeholder = "Betreff des Kunden";
$value->m_placeholder = "Nachricht des Kunden";
if(isset($data['customer_mail_id']) && $customer_mail = CustomerMail::find($data['customer_mail_id'])){

View file

@ -134,7 +134,7 @@ class LeadRepository extends BaseRepository {
$data = [
'booking_date' => date('Y-m-d'), //now
'customer_id' => $this->model->customer->id,
'lead_id' => $this->model->id,
'inquiry_id' => $this->model->id,
'new_drafts' => 1,
'sf_guard_user_id' => $this->model->sf_guard_user_id,
'branch_id' => 4,

View file

@ -1,127 +1,84 @@
<?php
namespace App\Services;
use App\Models\Airline;
use App\Models\Insurance;
use App\Models\CMSContent;
use App\Models\CustomerMail;
use App\Models\TravelCompany;
class Booking
{
private static $output_dirs = [];
public static function contentFiles(){
$booking_email_files = CMSContent::where('identifier', '=', 'booking-email-file')->get()->sortByDesc('pos')->pluck('slug', 'id');
return $booking_email_files;
public static function contentFiles(): \Illuminate\Support\Collection
{
return CMSContent::where('identifier', '=', 'booking-email-file')
->get()
->sortByDesc('pos')
->pluck('slug', 'id');
}
public static function setOutputDirs($dir, $subdir){
self::$output_dirs[$dir][] = $subdir;
public static function setOutputDirs(string $dir, string $subdir): void
{
MailDirService::setOutputDir($dir, $subdir);
}
public static function getMailDirNotInOutput($booking_id, $dir){
$is_o_dirs = isset(self::$output_dirs[$dir]) ? self::$output_dirs[$dir] : [];
$ret = [];
$CustomerMails = CustomerMail::whereBookingId($booking_id)->whereDir($dir)->get();
if($CustomerMails){
foreach($CustomerMails as $CustomerMail){
if(!in_array($CustomerMail->subdir, $is_o_dirs)){
$ret[] = $CustomerMail->subdir;
}
/**
* @return string[]
*/
public static function getMailDirNotInOutput(int $bookingId, string $dir): array
{
$mails = CustomerMail::whereBookingId($bookingId)->whereDir($dir)->get();
return MailDirService::getMailDirsNotInOutput($mails, $dir);
}
public static function getCustomerMailDirs(): \Illuminate\Database\Eloquent\Collection
{
return MailDirService::getCustomerMailDirs();
}
public static function getCustomerMailDir(int $id): ?\App\Models\CMSContent
{
return MailDirService::getCustomerMailDir($id);
}
public static function getCustomerMailName(\App\Models\CMSContent $mailDir, int $mailDirId): string
{
return MailDirService::getCustomerMailName($mailDir, $mailDirId);
}
public static function getCustomerMailEmails(\App\Models\CMSContent $mailDir, int $mailDirId): array|string
{
return MailDirService::getCustomerMailEmails($mailDir, $mailDirId);
}
public static function getBookingInstructionPDFName(\App\Models\FewoLodging $fewo): string
{
return "HINWEISE-FERIENWOHNUNG-" . $fewo->pdf_name . ".pdf";
}
public static function getBookingCMSContent(\App\Models\CMSContent $content, string $identifier): \Illuminate\Database\Eloquent\Collection
{
return CMSContent::where('identifier', '=', $identifier)
->where('integer', $content->id)
->get()
->sortBy('pos');
}
public static function getBookingCMSContentForPDF(string $identifierContent, string $identifier): array
{
$pdfContent = [];
$contents = CMSContent::where('identifier', '=', $identifierContent)->get()->sortBy('pos');
foreach ($contents as $content) {
if ($content->decimal > 0) {
$pdfContent[] = $content;
}
}
return $ret;
}
public static function getCustomerMailDirs(){
$customer_mail_dirs = CMSContent::where('identifier', '=', 'customer-mail-dirs')->get()->sortBy('pos');
return $customer_mail_dirs;
}
public static function getCustomerMailDir($id){
return CMSContent::where('identifier', '=', 'customer-mail-dirs')->where('pos', '=', $id)->first();
}
public static function getCustomerMailName($customer_mail_dir, $mail_dir_id){
switch ($customer_mail_dir->getArrayContent('model')){
case 'TravelCountry':
$model = \App\Models\Sym\TravelCountry::find($mail_dir_id);
break;
case 'Airline':
$model = Airline::find($mail_dir_id);
break;
case 'Insurance':
$model = Insurance::find($mail_dir_id);
break;
case 'TravelCompany':
$model = TravelCompany::find($mail_dir_id);
break;
default:
return '';
}
if($model){
if($customer_mail_dir->getArrayContent('model') === 'TravelCountry'){
return $model->mail_dir_name;
}
return $model->name;
}
return "";
}
public static function getCustomerMailEmails($customer_mail_dir, $mail_dir_id){
switch ($customer_mail_dir->getArrayContent('model')){
case 'TravelCountry':
$model = \App\Models\Sym\TravelCountry::find($mail_dir_id);
break;
case 'Airline':
$model = Airline::find($mail_dir_id);
break;
case 'Insurance':
$model = Insurance::find($mail_dir_id);
break;
case 'TravelCompany':
$model = TravelCompany::find($mail_dir_id);
break;
default:
//direkt from CMSContent
return $customer_mail_dir->getArrayContent('emails');
}
if($model){
return $model->contact_emails;
}
return [];
}
public static function getBookingInstructionPDFName($fewo){
return "HINWEISE-FERIENWOHNUNG-".$fewo->pdf_name.".pdf";
}
public static function getBookingCMSContent($content, $identifier){
return CMSContent::where('identifier', '=', $identifier)->where('integer', $content->id)->get()->sortBy('pos');
}
public static function getBookingCMSContentForPDF($identifier_content, $identifier){
$pdf_content = [];
$contents = CMSContent::where('identifier', '=', $identifier_content)->get()->sortBy('pos');
foreach ($contents as $content){
if($content->decimal > 0){ //in_pdf
$pdf_content[] = $content;
}
if($fewo_contents = self::getBookingCMSContent($content, $identifier)){
foreach ($fewo_contents as $fewo_content){
if($fewo_content->decimal > 0){ //in_pdf
$pdf_content[] = $fewo_content;
}
foreach (self::getBookingCMSContent($content, $identifier) as $fewoContent) {
if ($fewoContent->decimal > 0) {
$pdfContent[] = $fewoContent;
}
}
}
return $pdf_content;
return $pdfContent;
}
}
}

View file

@ -53,7 +53,7 @@ class BookingImport
$data = [
'booking_date' => $travel_booking->created->format('Y-m-d'),
'customer_id' => $customer->id,
'lead_id' => $lead->id,
'inquiry_id' => $lead->id,
'new_drafts' => $travel_booking->drafts === null ? 0 : 1,
'sf_guard_user_id' => 15,
'branch_id' => 4,

View file

@ -1,124 +1,51 @@
<?php
namespace App\Services;
use App\Models\Airline;
use App\Models\CMSContent;
use App\Models\Insurance;
use App\Models\LeadMail;
use App\Models\TravelCompany;
class Lead
{
private static $output_dirs = [];
public static function contentFiles(){
$lead_files = CMSContent::where('identifier', '=', 'lead-email-file')->get()->sortByDesc('pos')->pluck('slug', 'id');
return $lead_files;
public static function contentFiles(): \Illuminate\Support\Collection
{
return CMSContent::where('identifier', '=', 'lead-email-file')
->get()
->sortByDesc('pos')
->pluck('slug', 'id');
}
public static function setOutputDirs($dir, $subdir){
self::$output_dirs[$dir][] = $subdir;
public static function setOutputDirs(string $dir, string $subdir): void
{
MailDirService::setOutputDir($dir, $subdir);
}
public static function getMailDirNotInOutput($lead_id, $dir){
$is_o_dirs = isset(self::$output_dirs[$dir]) ? self::$output_dirs[$dir] : [];
$ret = [];
$LeadMails = LeadMail::whereLeadId($lead_id)->whereDir($dir)->get();
if($LeadMails){
foreach($LeadMails as $LeadMail){
if(!in_array($LeadMail->subdir, $is_o_dirs)){
$ret[] = $LeadMail->subdir;
}
}
}
return $ret;
}
public static function getCustomerMailDirs(){
$customer_mail_dirs = CMSContent::where('identifier', '=', 'customer-mail-dirs')->get()->sortBy('pos');
return $customer_mail_dirs;
/**
* @return string[]
*/
public static function getMailDirNotInOutput(int $leadId, string $dir): array
{
$mails = LeadMail::whereLeadId($leadId)->whereDir($dir)->get();
return MailDirService::getMailDirsNotInOutput($mails, $dir);
}
public static function getCustomerMailDir($id){
return CMSContent::where('identifier', '=', 'customer-mail-dirs')->where('pos', '=', $id)->first();
public static function getCustomerMailDirs(): \Illuminate\Database\Eloquent\Collection
{
return MailDirService::getCustomerMailDirs();
}
public static function getCustomerMailName($lead_mail_dir, $mail_dir_id){
switch ($lead_mail_dir->getArrayContent('model')){
case 'TravelCountry':
$model = \App\Models\Sym\TravelCountry::find($mail_dir_id);
break;
case 'Airline':
$model = Airline::find($mail_dir_id);
break;
case 'Insurance':
$model = Insurance::find($mail_dir_id);
break;
case 'TravelCompany':
$model = TravelCompany::find($mail_dir_id);
break;
default:
return '';
}
if($model){
if($lead_mail_dir->getArrayContent('model') === 'TravelCountry'){
return $model->mail_dir_name;
}
return $model->name;
}
return "";
public static function getCustomerMailDir(int $id): ?\App\Models\CMSContent
{
return MailDirService::getCustomerMailDir($id);
}
public static function getCustomerMailEmails($lead_mail_dir, $mail_dir_id){
switch ($lead_mail_dir->getArrayContent('model')){
case 'TravelCountry':
$model = \App\Models\Sym\TravelCountry::find($mail_dir_id);
break;
case 'Airline':
$model = Airline::find($mail_dir_id);
break;
case 'Insurance':
$model = Insurance::find($mail_dir_id);
break;
case 'TravelCompany':
$model = TravelCompany::find($mail_dir_id);
break;
default:
//direkt from CMSContent
return $lead_mail_dir->getArrayContent('emails');
}
if($model){
return $model->contact_emails;
}
return [];
public static function getCustomerMailName(\App\Models\CMSContent $mailDir, int $mailDirId): string
{
return MailDirService::getCustomerMailName($mailDir, $mailDirId);
}
/*public static function getFeWoInstructionPDFName($fewo){
return "HINWEISE-FERIENWOHNUNG-".$fewo->pdf_name.".pdf";
public static function getCustomerMailEmails(\App\Models\CMSContent $mailDir, int $mailDirId): array|string
{
return MailDirService::getCustomerMailEmails($mailDir, $mailDirId);
}
public static function getFeWoCMSContent($content, $identifier_fewo){
return CMSContent::where('identifier', '=', $identifier_fewo)->where('integer', $content->id)->get()->sortBy('pos');
}
public static function getFeWoCMSContentForPDF($identifier_content, $identifier_fewo){
$pdf_content = [];
$contents = CMSContent::where('identifier', '=', $identifier_content)->get()->sortBy('pos');
foreach ($contents as $content){
if($content->decimal > 0){ //in_pdf
$pdf_content[] = $content;
}
if($fewo_contents = BookingFewo::getFeWoCMSContent($content, $identifier_fewo)){
foreach ($fewo_contents as $fewo_content){
if($fewo_content->decimal > 0){ //in_pdf
$pdf_content[] = $fewo_content;
}
}
}
}
return $pdf_content;
}*/
}
}

View file

@ -0,0 +1,92 @@
<?php
namespace App\Services;
use App\Models\Airline;
use App\Models\CMSContent;
use App\Models\Insurance;
use App\Models\TravelCompany;
class MailDirService
{
private static array $outputDirs = [];
public static function setOutputDir(string $dir, string $subdir): void
{
self::$outputDirs[$dir][] = $subdir;
}
/**
* @return string[]
*/
public static function getCustomerMailDirs(): \Illuminate\Database\Eloquent\Collection
{
return CMSContent::where('identifier', '=', 'customer-mail-dirs')->get()->sortBy('pos');
}
public static function getCustomerMailDir(int $id): ?CMSContent
{
return CMSContent::where('identifier', '=', 'customer-mail-dirs')->where('pos', '=', $id)->first();
}
public static function getCustomerMailName(CMSContent $mailDir, int $mailDirId): string
{
$model = self::resolveModel($mailDir, $mailDirId);
if ($model === null) {
return '';
}
if ($mailDir->getArrayContent('model') === 'TravelCountry') {
return $model->mail_dir_name ?? '';
}
return $model->name ?? '';
}
/**
* @return array<string>|string
*/
public static function getCustomerMailEmails(CMSContent $mailDir, int $mailDirId): array|string
{
$model = self::resolveModel($mailDir, $mailDirId);
if ($model === null) {
// Default: emails come directly from CMSContent
return $mailDir->getArrayContent('emails') ?? [];
}
return $model->contact_emails ?? [];
}
/**
* Returns subdirs from a mail collection that were not already in the output.
*
* @param \Illuminate\Database\Eloquent\Collection $mails Collection of CustomerMail or LeadMail
* @return string[]
*/
public static function getMailDirsNotInOutput(iterable $mails, string $dir): array
{
$processed = self::$outputDirs[$dir] ?? [];
$result = [];
foreach ($mails as $mail) {
if (!in_array($mail->subdir, $processed, true)) {
$result[] = $mail->subdir;
}
}
return $result;
}
private static function resolveModel(CMSContent $mailDir, int $mailDirId): mixed
{
return match ($mailDir->getArrayContent('model')) {
'TravelCountry' => \App\Models\Sym\TravelCountry::find($mailDirId),
'Airline' => Airline::find($mailDirId),
'Insurance' => Insurance::find($mailDirId),
'TravelCompany' => TravelCompany::find($mailDirId),
default => null,
};
}
}

View file

@ -221,7 +221,7 @@ class Passolution
if(!Storage::disk('public')->exists( $this->pdf_dir )){
Storage::disk('public')->makeDirectory($this->pdf_dir); //creates directory
}
$this->pdf_path = Storage::disk('public')->getAdapter()->getPathPrefix();
$this->pdf_path = Storage::disk('public')->path('');
}
}

View file

@ -2,6 +2,8 @@
namespace App\Services;
use Carbon\Carbon;
class Util
{
@ -51,18 +53,17 @@ class Util
public static function _format_date($date, $to = 'date')
{
if ($to === 'datetime') {
return \Carbon::parse($date)->format(\Util::formatDateTimeDB());
return Carbon::parse($date)->format(self::formatDateTimeDB());
}
//date
return \Carbon::parse($date)->format(\Util::formatDateDB());
return Carbon::parse($date)->format(self::formatDateDB());
}
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 - H:i');
}
return \Carbon::parse($date)->format('Y-m-d');
return Carbon::parse($date)->format('Y-m-d');
}
public static function _format_number($value)

View file

@ -11,7 +11,7 @@ use Illuminate\Support\Facades\Crypt;
use Illuminate\Notifications\Notifiable;
use Illuminate\Database\Eloquent\SoftDeletes;
use Illuminate\Foundation\Auth\User as Authenticatable;
use PhpOffice\PhpSpreadsheet\Calculation\Statistical\Distributions\F;
use Illuminate\Database\Eloquent\Factories\HasFactory;
/**
* App\User
@ -86,9 +86,7 @@ use PhpOffice\PhpSpreadsheet\Calculation\Statistical\Distributions\F;
*/
class User extends Authenticatable
{
use HasApiTokens, Notifiable;
use SoftDeletes;
use HasApiTokens, HasFactory, Notifiable, SoftDeletes;
protected $connection = 'mysql';