From ba487458094bb363853cbaf63187cda21ac66087 Mon Sep 17 00:00:00 2001 From: Kevin Adametz Date: Wed, 22 Apr 2026 16:01:27 +0200 Subject: [PATCH] phase 2 dev --- .../BookingsAuditStornoPriceTotal.php | 156 ++ .../Commands/ContactsFindDuplicates.php | 6 +- .../Commands/ContactsMergeDuplicates.php | 12 +- .../Commands/SyncNewsletterKulturreisen.php | 3 +- .../Controllers/API/BookingController.php | 4 +- .../Admin/ReportBookingController.php | 14 +- .../Controllers/Admin/ReportController.php | 30 +- .../Admin/ReportLeadsController.php | 10 +- .../Admin/ReportProviderController.php | 16 +- app/Http/Controllers/ContactController.php | 28 +- app/Http/Controllers/CustomerController.php | 2 +- app/Http/Controllers/LeadController.php | 4 +- app/Http/Controllers/RequestController.php | 12 +- app/Models/Booking.php | 47 +- app/Models/Contact.php | 4 +- app/Models/Customer.php | 7 +- app/Models/Lead.php | 251 +-- app/Models/Offer.php | 148 +- app/Models/OfferAccessToken.php | 120 ++ app/Models/OfferFile.php | 99 + app/Models/OfferItem.php | 86 + app/Models/OfferTemplate.php | 77 + app/Models/OfferVersion.php | 126 ++ app/Repositories/BookingPDFRepository.php | 20 +- app/Repositories/BookingRepository.php | 5 +- app/Repositories/CustomerMailRepository.php | 14 +- app/Repositories/LeadRepository.php | 2 +- app/Services/Booking.php | 6 +- app/Services/BookingImport.php | 2 +- app/Services/Lead.php | 6 +- app/Services/MailDirService.php | 11 +- bootstrap/cache/config.php | 1708 ----------------- config/filesystems.php | 6 + ...001_phase2_rename_customer_to_contacts.php | 30 + ...200002_phase2_rename_lead_to_inquiries.php | 30 + ...2_rename_booking_lead_id_to_inquiry_id.php | 52 + ...ase3_create_participants_unified_table.php | 123 ++ ...002_phase3_drop_old_participant_tables.php | 40 + ...001_phase4_create_communications_table.php | 146 ++ ..._15_400002_phase4_create_notices_table.php | 94 + ...400003_phase4_create_attachments_table.php | 118 ++ ...4_phase4_drop_old_communication_tables.php | 45 + ...5_400005_phase4_drop_old_notice_tables.php | 35 + ...0006_phase4_drop_old_attachment_tables.php | 39 + .../2026_04_17_100001_create_offers_table.php | 71 + ..._17_100002_create_offer_versions_table.php | 81 + ..._04_17_100003_create_offer_items_table.php | 68 + ...17_100004_create_offer_templates_table.php | 70 + ..._04_17_100005_create_offer_files_table.php | 48 + ...00006_create_offer_access_tokens_table.php | 48 + ..._add_offer_refs_to_offers_and_bookings.php | 57 + .../phase2-offers-2026-04-17/restore.sh | 8 +- dev/customer-bookings/phase-2-live-deploy.md | 305 +++ dev/customer-bookings/umsetzung.md | 109 +- .../views/booking/_detail_price.blade.php | 19 + resources/views/contact/index.blade.php | 2 +- .../mail/modal-show-mail-inner.blade.php | 2 +- .../pdf/components/booking_head.blade.php | 2 +- .../pdf/components/booking_header.blade.php | 2 +- 59 files changed, 2692 insertions(+), 1994 deletions(-) create mode 100644 app/Console/Commands/BookingsAuditStornoPriceTotal.php create mode 100644 app/Models/OfferAccessToken.php create mode 100644 app/Models/OfferFile.php create mode 100644 app/Models/OfferItem.php create mode 100644 app/Models/OfferTemplate.php create mode 100644 app/Models/OfferVersion.php delete mode 100644 bootstrap/cache/config.php create mode 100644 database/migrations/2025_04_15_200001_phase2_rename_customer_to_contacts.php create mode 100644 database/migrations/2025_04_15_200002_phase2_rename_lead_to_inquiries.php create mode 100644 database/migrations/2025_04_15_200003_phase2_rename_booking_lead_id_to_inquiry_id.php create mode 100644 database/migrations/2025_04_15_300001_phase3_create_participants_unified_table.php create mode 100644 database/migrations/2025_04_15_300002_phase3_drop_old_participant_tables.php create mode 100644 database/migrations/2025_04_15_400001_phase4_create_communications_table.php create mode 100644 database/migrations/2025_04_15_400002_phase4_create_notices_table.php create mode 100644 database/migrations/2025_04_15_400003_phase4_create_attachments_table.php create mode 100644 database/migrations/2025_04_15_400004_phase4_drop_old_communication_tables.php create mode 100644 database/migrations/2025_04_15_400005_phase4_drop_old_notice_tables.php create mode 100644 database/migrations/2025_04_15_400006_phase4_drop_old_attachment_tables.php create mode 100644 database/migrations/2026_04_17_100001_create_offers_table.php create mode 100644 database/migrations/2026_04_17_100002_create_offer_versions_table.php create mode 100644 database/migrations/2026_04_17_100003_create_offer_items_table.php create mode 100644 database/migrations/2026_04_17_100004_create_offer_templates_table.php create mode 100644 database/migrations/2026_04_17_100005_create_offer_files_table.php create mode 100644 database/migrations/2026_04_17_100006_create_offer_access_tokens_table.php create mode 100644 database/migrations/2026_04_17_100007_add_offer_refs_to_offers_and_bookings.php create mode 100644 dev/customer-bookings/phase-2-live-deploy.md diff --git a/app/Console/Commands/BookingsAuditStornoPriceTotal.php b/app/Console/Commands/BookingsAuditStornoPriceTotal.php new file mode 100644 index 0000000..e7a6478 --- /dev/null +++ b/app/Console/Commands/BookingsAuditStornoPriceTotal.php @@ -0,0 +1,156 @@ +whereNotNull('canceled') + ->whereNotNull('price_canceled') + ->orderBy('id'); + + $ids = $this->option('id'); + if ($ids !== []) { + $query->whereIn('id', $ids); + } + + $mismatch = []; + $fixable = []; + + foreach ($query->cursor() as $booking) { + $pt = (float) $booking->getPriceTotalRaw(); + $pc = (float) $booking->getPriceCanceledRaw(); + if (abs($pt - $pc) <= self::EPS) { + continue; + } + + $hasStornoRow = $booking->booking_strono()->exists(); + $hasStornoDoc = $booking->hasDocument('storno'); + + $mismatch[] = [ + 'id' => $booking->id, + 'merlin_order_number' => $booking->merlin_order_number, + 'price' => $booking->getPriceRaw(), + 'price_canceled' => $pc, + 'price_total_db' => $pt, + 'price_balance' => $booking->getPriceBalanceRaw(), + 'canceled_pct' => $booking->getCanceledRaw(), + 'booking_storno_row' => $hasStornoRow ? 'ja' : 'nein', + 'storno_pdf' => $hasStornoDoc ? 'ja' : 'nein', + 'diff' => round($pt - $pc, 2), + ]; + $fixable[] = $booking->id; + } + + $this->warn('Storno mit gesetztem price_canceled, aber price_total weicht ab:'); + $this->newLine(); + + if ($mismatch === []) { + $this->info('Keine Abweichungen gefunden.'); + } else { + $this->table( + ['id', 'MyJack', 'price', 'price_canceled', 'price_total', 'diff', 'storno_row', 'storno_pdf'], + collect($mismatch)->map(function ($r) { + return [ + $r['id'], + $r['merlin_order_number'], + $r['price'], + $r['price_canceled'], + $r['price_total_db'], + $r['diff'], + $r['booking_storno_row'], + $r['storno_pdf'], + ]; + })->all() + ); + $this->newLine(); + $this->line('Anzahl: '.count($mismatch)); + } + + $exportPath = $this->option('export'); + if ($exportPath && $mismatch !== []) { + $path = $this->resolveExportPath($exportPath); + $fp = fopen($path, 'w'); + if ($fp === false) { + $this->error('Export nicht schreibbar: '.$path); + + return self::FAILURE; + } + fputcsv($fp, array_keys($mismatch[0]), ';'); + foreach ($mismatch as $row) { + fputcsv($fp, $row, ';'); + } + fclose($fp); + $this->info('CSV geschrieben: '.$path); + } + + if ($this->option('fix')) { + if ($fixable === []) { + $this->info('Nichts zu korrigieren.'); + + return self::SUCCESS; + } + if (! $this->confirm('price_total für '.count($fixable).' Buchung(en) auf price_canceled setzen?', true)) { + $this->warn('Abgebrochen.'); + + return self::SUCCESS; + } + + $updated = 0; + DB::transaction(function () use ($fixable, &$updated) { + foreach ($fixable as $id) { + $b = Booking::query()->lockForUpdate()->find($id); + if (! $b) { + continue; + } + $pc = (float) $b->getPriceCanceledRaw(); + $pt = (float) $b->getPriceTotalRaw(); + if (abs($pt - $pc) <= self::EPS) { + continue; + } + $b->price_total = round($pc, 2); + $b->save(); + $updated++; + } + }); + $this->info("Korrigiert: {$updated} Buchung(en)."); + } + + $this->newLine(); + $this->line('Hinweis: Buchungen mit canceled gesetzt, aber price_canceled NULL, werden hier nicht geprüft.'); + + return self::SUCCESS; + } + + private function resolveExportPath(string $exportPath): string + { + if ($exportPath[0] === '/' || preg_match('#^[A-Za-z]:\\\\#', $exportPath)) { + return $exportPath; + } + + return base_path(trim($exportPath, '/')); + } +} diff --git a/app/Console/Commands/ContactsFindDuplicates.php b/app/Console/Commands/ContactsFindDuplicates.php index cef982d..35ab2fb 100644 --- a/app/Console/Commands/ContactsFindDuplicates.php +++ b/app/Console/Commands/ContactsFindDuplicates.php @@ -37,7 +37,7 @@ class ContactsFindDuplicates extends Command // ── HIGH: gleiche E-Mail ────────────────────────────────────────── if ($this->shouldCheck('HIGH')) { - $emailDupes = DB::table('customer') + $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', '!=', '') @@ -60,7 +60,7 @@ class ContactsFindDuplicates extends Command // ── MEDIUM: Name + Vorname + Geburtsdatum ──────────────────────── if ($this->shouldCheck('MEDIUM')) { - $nameBdDupes = DB::table('customer') + $nameBdDupes = DB::table('contacts') ->select( 'name', 'firstname', 'birthdate', DB::raw('COUNT(*) as cnt'), @@ -88,7 +88,7 @@ class ContactsFindDuplicates extends Command // ── LOW: Name + Vorname + PLZ ───────────────────────────────────── if ($this->shouldCheck('LOW')) { - $nameZipDupes = DB::table('customer') + $nameZipDupes = DB::table('contacts') ->select( 'name', 'firstname', 'zip', DB::raw('COUNT(*) as cnt'), diff --git a/app/Console/Commands/ContactsMergeDuplicates.php b/app/Console/Commands/ContactsMergeDuplicates.php index e51ca46..9aeeca6 100644 --- a/app/Console/Commands/ContactsMergeDuplicates.php +++ b/app/Console/Commands/ContactsMergeDuplicates.php @@ -90,7 +90,7 @@ class ContactsMergeDuplicates extends Command private function findByEmail(): array { - return DB::table('customer') + return DB::table('contacts') ->select('email', DB::raw('GROUP_CONCAT(id ORDER BY updated_at DESC, id DESC) as ids')) ->whereNotNull('email') ->where('email', '!=', '') @@ -104,7 +104,7 @@ class ContactsMergeDuplicates extends Command private function findByNameBirthdate(): array { - return DB::table('customer') + return DB::table('contacts') ->select(DB::raw('GROUP_CONCAT(id ORDER BY updated_at DESC, id DESC) as ids')) ->whereNotNull('name') ->whereNotNull('firstname') @@ -119,7 +119,7 @@ class ContactsMergeDuplicates extends Command private function findByNameZip(): array { - return DB::table('customer') + return DB::table('contacts') ->select(DB::raw('GROUP_CONCAT(id ORDER BY updated_at DESC, id DESC) as ids')) ->whereNotNull('name') ->whereNotNull('firstname') @@ -173,11 +173,11 @@ class ContactsMergeDuplicates extends Command private function mergeInto(int $masterId, int $dupeId): void { // 1. Leads umhängen - $leadCount = DB::table('lead')->where('customer_id', $dupeId)->count(); + $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('lead') + DB::table('inquiries') ->where('customer_id', $dupeId) ->update(['customer_id' => $masterId]); } @@ -220,7 +220,7 @@ class ContactsMergeDuplicates extends Command // 5. Duplikat als zusammengeführt markieren if (!$this->dryRun) { - DB::table('customer') + DB::table('contacts') ->where('id', $dupeId) ->update([ 'merged_into_id' => $masterId, diff --git a/app/Console/Commands/SyncNewsletterKulturreisen.php b/app/Console/Commands/SyncNewsletterKulturreisen.php index c895505..a6c5fbc 100644 --- a/app/Console/Commands/SyncNewsletterKulturreisen.php +++ b/app/Console/Commands/SyncNewsletterKulturreisen.php @@ -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, ], ]); } diff --git a/app/Http/Controllers/API/BookingController.php b/app/Http/Controllers/API/BookingController.php index a020ad4..25b6c91 100755 --- a/app/Http/Controllers/API/BookingController.php +++ b/app/Http/Controllers/API/BookingController.php @@ -34,7 +34,9 @@ class BookingController extends Controller $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); diff --git a/app/Http/Controllers/Admin/ReportBookingController.php b/app/Http/Controllers/Admin/ReportBookingController.php index 6579c4c..0c518e9 100644 --- a/app/Http/Controllers/Admin/ReportBookingController.php +++ b/app/Http/Controllers/Admin/ReportBookingController.php @@ -172,10 +172,10 @@ class ReportBookingController extends Controller ->orderColumn('end_date', 'end_date $1') ->orderColumn('price', 'price $1') ->orderColumn('booking_date', 'booking_date $1') - ->orderColumn('customer.fullName', 'customer.firstname $1') - ->orderColumn('customer.firstname', 'customer.firstname $1') - ->orderColumn('customer.name', 'customer.name $1') - //->orderColumn('lead.status_id', 'lead.status_id $1') + ->orderColumn('customer.fullName', 'contacts.firstname $1') + ->orderColumn('customer.firstname', 'contacts.firstname $1') + ->orderColumn('customer.name', 'contacts.name $1') + //->orderColumn('lead.status_id', 'inquiries.status_id $1') //->orderColumn('is_cleared', 'is_cleared $1') ->rawColumns(['id', 'lead.status_id', 'service_provider.names']) ->make(true); @@ -384,9 +384,9 @@ class ReportBookingController extends Controller ->orderColumn('end_date', 'end_date $1') ->orderColumn('price', 'price $1') ->orderColumn('booking_date', 'booking_date $1') - ->orderColumn('customer.firstname', 'customer.firstname $1') - ->orderColumn('customer.name', 'customer.name $1') - //->orderColumn('lead.status_id', 'lead.status_id $1') + ->orderColumn('customer.firstname', 'contacts.firstname $1') + ->orderColumn('customer.name', 'contacts.name $1') + //->orderColumn('lead.status_id', 'inquiries.status_id $1') //->orderColumn('is_cleared', 'is_cleared $1') ->rawColumns(['id', 'old_crm', 'check_total', 'lead.status_id']) ->make(true); diff --git a/app/Http/Controllers/Admin/ReportController.php b/app/Http/Controllers/Admin/ReportController.php index 7b46918..033d8b9 100755 --- a/app/Http/Controllers/Admin/ReportController.php +++ b/app/Http/Controllers/Admin/ReportController.php @@ -189,10 +189,10 @@ class ReportController extends Controller ->orderColumn('end_date', 'end_date $1') ->orderColumn('price', 'price $1') ->orderColumn('booking_date', 'booking_date $1') - ->orderColumn('customer.fullName', 'customer.firstname $1') - ->orderColumn('customer.firstname', 'customer.firstname $1') - ->orderColumn('customer.name', 'customer.name $1') - //->orderColumn('lead.status_id', 'lead.status_id $1') + ->orderColumn('customer.fullName', 'contacts.firstname $1') + ->orderColumn('customer.firstname', 'contacts.firstname $1') + ->orderColumn('customer.name', 'contacts.name $1') + //->orderColumn('lead.status_id', 'inquiries.status_id $1') //->orderColumn('is_cleared', 'is_cleared $1') ->rawColumns(['id', 'lead.status_id', 'service_provider.names']) ->make(true); @@ -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 : "", @@ -725,9 +725,9 @@ class ReportController extends Controller ->orderColumn('end_date', 'end_date $1') ->orderColumn('price', 'price $1') ->orderColumn('booking_date', 'booking_date $1') - ->orderColumn('customer.firstname', 'customer.firstname $1') - ->orderColumn('customer.name', 'customer.name $1') - //->orderColumn('lead.status_id', 'lead.status_id $1') + ->orderColumn('customer.firstname', 'contacts.firstname $1') + ->orderColumn('customer.name', 'contacts.name $1') + //->orderColumn('lead.status_id', 'inquiries.status_id $1') //->orderColumn('is_cleared', 'is_cleared $1') ->rawColumns(['id', 'old_crm', 'check_total', 'lead.status_id']) ->make(true); diff --git a/app/Http/Controllers/Admin/ReportLeadsController.php b/app/Http/Controllers/Admin/ReportLeadsController.php index 2f2b888..232ddee 100644 --- a/app/Http/Controllers/Admin/ReportLeadsController.php +++ b/app/Http/Controllers/Admin/ReportLeadsController.php @@ -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); @@ -161,9 +161,9 @@ class ReportLeadsController extends Controller $orderByNum = [ 0 => 'id', 1 => 'customer_id', - 2 => 'customer.firstname', - 3 => 'customer.name', - 4 => 'customer.email', + 2 => 'contacts.firstname', + 3 => 'contacts.name', + 4 => 'contacts.email', 5 => 'request_date', 6 => 'travel_country', 7 => 'sf_guard_user.last_name', diff --git a/app/Http/Controllers/Admin/ReportProviderController.php b/app/Http/Controllers/Admin/ReportProviderController.php index 297a96a..d9dbf9c 100644 --- a/app/Http/Controllers/Admin/ReportProviderController.php +++ b/app/Http/Controllers/Admin/ReportProviderController.php @@ -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() : "", diff --git a/app/Http/Controllers/ContactController.php b/app/Http/Controllers/ContactController.php index 08ef241..3ddedc9 100644 --- a/app/Http/Controllers/ContactController.php +++ b/app/Http/Controllers/ContactController.php @@ -157,11 +157,11 @@ class ContactController extends Controller } DB::transaction(function () use ($masterId, $dupeId) { - DB::table('lead')->where('customer_id', $dupeId)->update(['customer_id' => $masterId]); + 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('customer')->where('id', $dupeId)->update([ + DB::table('contacts')->where('id', $dupeId)->update([ 'merged_into_id' => $masterId, 'merged_at' => now(), ]); @@ -175,16 +175,16 @@ class ContactController extends Controller private function countDuplicateGroups(string $type): int { return match ($type) { - 'email' => DB::table('customer')->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('customer')->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('customer')->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(), + '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('customer') + 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') @@ -194,7 +194,7 @@ class ContactController extends Controller private function findByNameBirthdate(): array { - return DB::table('customer') + 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') @@ -204,7 +204,7 @@ class ContactController extends Controller private function findByNameZip(): array { - return DB::table('customer') + 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', '!=', '') @@ -228,8 +228,8 @@ class ContactController extends Controller // 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('customer.*')->withCount(['leads', 'bookings']) - : Contact::select('customer.*')->withCount(['leads', 'bookings']); + ? 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')) { @@ -271,11 +271,11 @@ class ContactController extends Controller ->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) { + ->orderColumn('contacts.id', 'contacts.id $1') + ->orderColumn('deleted_at', 'contacts.deleted_at $1') + ->filterColumn('contacts.id', function ($query, $keyword) { if ($keyword !== '') { - $query->where('customer.id', 'LIKE', '%' . $keyword . '%'); + $query->where('contacts.id', 'LIKE', '%' . $keyword . '%'); } }) ->filterColumn('name', function ($query, $keyword) { diff --git a/app/Http/Controllers/CustomerController.php b/app/Http/Controllers/CustomerController.php index f52fb12..fc149ed 100755 --- a/app/Http/Controllers/CustomerController.php +++ b/app/Http/Controllers/CustomerController.php @@ -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) { diff --git a/app/Http/Controllers/LeadController.php b/app/Http/Controllers/LeadController.php index 60d7202..a4d99fb 100755 --- a/app/Http/Controllers/LeadController.php +++ b/app/Http/Controllers/LeadController.php @@ -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); diff --git a/app/Http/Controllers/RequestController.php b/app/Http/Controllers/RequestController.php index a7f21e5..a02f566 100755 --- a/app/Http/Controllers/RequestController.php +++ b/app/Http/Controllers/RequestController.php @@ -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 '' . $booking->id . ''; }) ->addColumn('action_lead_edit', function (Booking $booking) { - return ''; + return ''; }) ->addColumn('lead_id', function (Booking $booking) { - return '' . $booking->lead_id . ''; + return '' . $booking->inquiry_id . ''; }) ->addColumn('travel_country_id', function (Booking $booking) { return '' . ($booking->travel_country_id ? $booking->travel_country->name : "-") . ''; @@ -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') diff --git a/app/Models/Booking.php b/app/Models/Booking.php index 3d61b89..80811b3 100644 --- a/app/Models/Booking.php +++ b/app/Models/Booking.php @@ -19,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 @@ -212,7 +212,8 @@ class Booking extends Model 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', @@ -256,7 +257,8 @@ class Booking extends Model protected $fillable = [ 'booking_date', 'customer_id', - 'lead_id', + 'inquiry_id', + 'offer_id', 'new_drafts', 'sf_guard_user_id', 'branch_id', @@ -393,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() @@ -765,8 +787,6 @@ class Booking extends Model $total_children += $prices['children']; } - - if ($travel_draft_item) { $travel_draft_item->setPriceAdultRaw($travel_price_adult); $travel_draft_item->setPriceChildrenRaw($travel_price_children); @@ -775,10 +795,23 @@ class Booking extends Model $travel_draft_item->save(); } $this->price = $total_adult + $total_children; - $this->price_total = $this->getPriceRaw() + $this->getServiceTotal(true); + $this->setPriceTotalForCurrentState(); $this->save(); } + /** + * Gesamtpreis Reise (price_total): bei Storno mit gesetztem Storno-Betrag = price_canceled + * (wie nach createPDF_Storno in BookingPDFRepository), sonst Reisepreis + Vermittlung. + */ + public function setPriceTotalForCurrentState(): void + { + if ($this->isCanceled() && $this->attributes['price_canceled'] !== null) { + $this->price_total = round((float) $this->getPriceCanceledRaw(), 2); + return; + } + $this->price_total = round((float) $this->getPriceRaw() + (float) $this->getServiceTotal(true), 2); + } + public function getPriceAttribute() { return Util::_number_format($this->attributes['price']); diff --git a/app/Models/Contact.php b/app/Models/Contact.php index 6d941e8..2cd1868 100644 --- a/app/Models/Contact.php +++ b/app/Models/Contact.php @@ -18,7 +18,7 @@ use Illuminate\Database\Eloquent\SoftDeletes; * - merged_into_id + merged_at in $fillable * - mergedInto() / mergedContacts() Beziehungen * - * Tabellen-Name: 'customer' (wird in Phase 2 in 'contacts' umbenannt). + * Tabellen-Name: 'contacts' (nach Phase 2 — RENAME TABLE customer → contacts). * * @property int $id * @property int|null $salutation_id @@ -50,7 +50,7 @@ class Contact extends Model protected $connection = 'mysql'; - protected $table = 'customer'; + protected $table = 'contacts'; protected $casts = [ 'salutation_id' => 'int', diff --git a/app/Models/Customer.php b/app/Models/Customer.php index 436b3ee..9853334 100644 --- a/app/Models/Customer.php +++ b/app/Models/Customer.php @@ -88,7 +88,12 @@ class Customer extends Model 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', diff --git a/app/Models/Lead.php b/app/Models/Lead.php index 227bd38..f7ddf33 100644 --- a/app/Models/Lead.php +++ b/app/Models/Lead.php @@ -108,11 +108,16 @@ use Illuminate\Database\Eloquent\Collection; */ class Lead extends Model { - use HasFactory; + use HasFactory; - protected $connection = 'mysql'; + 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', @@ -121,8 +126,8 @@ class Lead extends Model 'travelagenda_id' => 'int', 'sf_guard_user_id' => 'int', 'is_closed' => 'bool', - 'is_rebook' => 'bool', - 'initialcontacttype_id' => 'int', + 'is_rebook' => 'bool', + 'initialcontacttype_id' => 'int', 'searchengine_id' => 'int', 'status_id' => 'int', 'website_id' => 'int', @@ -149,7 +154,7 @@ class Lead extends Model 'remarks', 'sf_guard_user_id', 'is_closed', - 'is_rebook', + 'is_rebook', 'initialcontacttype_id', 'searchengine_id', 'searchengine_keywords', @@ -164,24 +169,24 @@ class Lead extends Model 'participant_birthdate', 'participant_salutation_id' ]; - protected $passolutionPDFs = []; - + protected $passolutionPDFs = []; + public static $lead_mail_dirs = [ - 11 => ['name' => 'Entwürfe', 'icon'=>'ion-md-create'], - 12 => ['name' => 'Papierkorb', 'icon'=>'ion-md-trash'], - ]; + 11 => ['name' => 'Entwürfe', 'icon' => 'ion-md-create'], + 12 => ['name' => 'Papierkorb', 'icon' => 'ion-md-trash'], + ]; - public function updateNextDueDate($date = false){ + public function updateNextDueDate($date = false) + { - if(!$date){ - $carbon = Carbon::now(); - }else{ + if (!$date) { + $carbon = Carbon::now(); + } else { $carbon = Carbon::parse($date); } - $this->next_due_date = $carbon->modify('+ '.$this->status->handling_days.' days')->format("Y-m-d"); + $this->next_due_date = $carbon->modify('+ ' . $this->status->handling_days . ' days')->format("Y-m-d"); $this->save(); - - } + } public function customer() { return $this->belongsTo(Customer::class); @@ -222,18 +227,18 @@ class Lead extends Model return $this->belongsTo(TravelCategory::class, 'travelcategory_id'); } - //on crm - public function travel_country_crm() - { - return $this->belongsTo('App\Models\Sym\TravelCountry', 'travelcountry_id', 'id'); - } + //on crm + public function travel_country_crm() + { + return $this->belongsTo('App\Models\Sym\TravelCountry', 'travelcountry_id', 'id'); + } - //on stern other DB - public function travel_country() - { - return $this->belongsTo('App\Models\TravelCountry', 'travelcountry_id', 'crm_id'); - } + //on stern other DB + public function travel_country() + { + return $this->belongsTo('App\Models\TravelCountry', 'travelcountry_id', 'crm_id'); + } public function website() @@ -243,7 +248,8 @@ class Lead extends Model public function bookings() { - return $this->hasMany(Booking::class); + // Modul 3 Phase 2: FK heißt jetzt inquiry_id (vormals lead_id) + return $this->hasMany(Booking::class, 'inquiry_id'); } public function inquiries() @@ -267,149 +273,156 @@ class Lead extends Model } public function lead_files() - { + { //no lead_mail_id - return $this->hasMany(LeadFile::class, 'lead_id')->where('lead_mail_id', null); - } + return $this->hasMany(LeadFile::class, 'lead_id')->where('lead_mail_id', null); + } - public function lead_mails() - { - return $this->hasMany(LeadMail::class, 'lead_id', 'id'); - } + public function lead_mails() + { + return $this->hasMany(LeadMail::class, 'lead_id', 'id'); + } - public function lead_mails_sent_at() - { - return $this->hasMany(LeadMail::class, 'lead_id')->orderBy('sent_at', 'ASC'); - } + public function lead_mails_sent_at() + { + return $this->hasMany(LeadMail::class, 'lead_id')->orderBy('sent_at', 'ASC'); + } - public function lead_mail_last() - { - return $this->hasOne(LeadMail::class, 'lead_id')->latest(); - } + public function lead_mail_last() + { + return $this->hasOne(LeadMail::class, 'lead_id')->latest(); + } - public function lead_notices() - { - return $this->hasMany(LeadNotice::class, 'lead_id')->orderBy('updated_at', 'DESC'); - } + public function lead_notices() + { + return $this->hasMany(LeadNotice::class, 'lead_id')->orderBy('updated_at', 'DESC'); + } - public static function getSfGuardUserArray(){ - return SfGuardUser::where('is_active', 1)->get()->pluck('fullname', 'id'); - } + public static function getSfGuardUserArray() + { + return SfGuardUser::where('is_active', 1)->get()->pluck('fullname', 'id'); + } - public static function getTravelCountryArray($emtpy = false){ + public static function getTravelCountryArray($emtpy = false) + { $TravelCountry = TravelCountry::where('active_backend', 1)->orderBy('name')->get()->pluck('name', 'id'); - return $emtpy ? $TravelCountry->prepend('-', 0) : $TravelCountry; + return $emtpy ? $TravelCountry->prepend('-', 0) : $TravelCountry; + } - } - - public static function getTravelCategoryArray($emtpy = false){ + public static function getTravelCategoryArray($emtpy = false) + { $TravelCategory = TravelCategory::orderBy('name')->get()->pluck('name', 'id'); - return $emtpy ? $TravelCategory->prepend('-', 0) : $TravelCategory; - + return $emtpy ? $TravelCategory->prepend('-', 0) : $TravelCategory; } - public static function getTravelAgendaArray($emtpy = false){ + public static function getTravelAgendaArray($emtpy = false) + { $TravelAgenda = TravelAgenda::orderBy('name')->get()->pluck('name', 'id'); - return $emtpy ? $TravelAgenda->prepend('-', 0) : $TravelAgenda; + return $emtpy ? $TravelAgenda->prepend('-', 0) : $TravelAgenda; } - public static function getStatusArray($emtpy = false){ + public static function getStatusArray($emtpy = false) + { $Status = Status::orderBy('name')->get()->pluck('name', 'id'); - return $emtpy ? $Status->prepend('-', 0) : $Status; + return $emtpy ? $Status->prepend('-', 0) : $Status; } public function getStatusBadge($booking = null) { - if($this->status_id && $this->status){ + if ($this->status_id && $this->status) { $color = $this->status->color; $icon = ""; - if($this->status_id == 14 && $this->is_rebook){ + if ($this->status_id == 14 && $this->is_rebook) { $color = '#94ae59'; $icon = ' '; } - if($this->status_id == 14 && !$this->is_rebook){ - $icon = ' '; + if ($this->status_id == 14 && !$this->is_rebook) { + $icon = ' '; } - if($this->status_id == 15){ + if ($this->status_id == 15) { $icon = ' '; - if($booking && $booking->lawyer_date){ - return ''.$icon.$booking->lawyer_date->format('d.m.Y').''; + if ($booking && $booking->lawyer_date) { + return '' . $icon . $booking->lawyer_date->format('d.m.Y') . ''; } } - return ''.$icon.$this->status->name.''; + return '' . $icon . $this->status->name . ''; } return '-'; } - public function getTravelCountryDestco($badge = true){ + public function getTravelCountryDestco($badge = true) + { $out = ""; - if($this->bookings->count()){ + if ($this->bookings->count()) { $out .= $badge ? '' : ''; - foreach ($this->bookings as $booking){ - if($booking->travel_country_id && $booking->travel_country) { + foreach ($this->bookings as $booking) { + if ($booking->travel_country_id && $booking->travel_country) { $out .= $booking->travel_country->destco; } } $out .= $badge ? '' : ''; return $out; } - if($this->travel_country){ - return $badge ? ''.$this->travel_country->destco.'' : $this->travel_country->destco; + if ($this->travel_country) { + return $badge ? '' . $this->travel_country->destco . '' : $this->travel_country->destco; } return "-"; } - public function countLeadMailsBy($dir, $subdir=false){ - if($dir === 11){ - return $this->lead_mails->where('draft', true)->where('dir', '!=', 12)->count(); - } - if($subdir){ - return $this->lead_mails->where('dir', $dir)->where('subdir', $subdir)->count(); - } - return $this->lead_mails->where('dir', $dir)->count(); - } + public function countLeadMailsBy($dir, $subdir = false) + { + if ($dir === 11) { + return $this->lead_mails->where('draft', true)->where('dir', '!=', 12)->count(); + } + if ($subdir) { + return $this->lead_mails->where('dir', $dir)->where('subdir', $subdir)->count(); + } + return $this->lead_mails->where('dir', $dir)->count(); + } - public function getPassolutionPDF($create = false, $resync = false){ + public function getPassolutionPDF($create = false, $resync = false) + { - $nats = []; + $nats = []; - if(count($this->passolutionPDFs)){ - return $this->passolutionPDFs; - } + if (count($this->passolutionPDFs)) { + return $this->passolutionPDFs; + } - if(!$this->travel_country){ - return $this->passolutionPDFs; - } - - $destco = $this->travel_country->destco; + if (!$this->travel_country) { + return $this->passolutionPDFs; + } + + $destco = $this->travel_country->destco; //default no travel_nationality $nats['de'] = 'de'; - - if($this->lead_participants->count()){ - foreach ($this->lead_participants as $participant){ - if($participant->travel_nationality){ - $nats[$participant->travel_nationality->nat] = $participant->travel_nationality->nat; - } - } - } - if(empty($nats)){ - $nats['de'] = 'de'; - } - foreach ($nats as $nat){ - $data = [ - 'nat' => $nat, - 'destco' => $destco, - ]; - $passolution = new Passolution($data); - $this->passolutionPDFs[] = $passolution->findOrCreatePDF($create, $resync); - } - return $this->passolutionPDFs; - } - public function resyncPassolutionPDF(){ - return $this->getPassolutionPDF(true, true); - } + if ($this->lead_participants->count()) { + foreach ($this->lead_participants as $participant) { + if ($participant->travel_nationality) { + $nats[$participant->travel_nationality->nat] = $participant->travel_nationality->nat; + } + } + } + if (empty($nats)) { + $nats['de'] = 'de'; + } + foreach ($nats as $nat) { + $data = [ + 'nat' => $nat, + 'destco' => $destco, + ]; + $passolution = new Passolution($data); + $this->passolutionPDFs[] = $passolution->findOrCreatePDF($create, $resync); + } + return $this->passolutionPDFs; + } + + public function resyncPassolutionPDF() + { + return $this->getPassolutionPDF(true, true); + } } diff --git a/app/Models/Offer.php b/app/Models/Offer.php index dd276b9..02b503c 100644 --- a/app/Models/Offer.php +++ b/app/Models/Offer.php @@ -1,56 +1,132 @@ '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); + } } diff --git a/app/Models/OfferAccessToken.php b/app/Models/OfferAccessToken.php new file mode 100644 index 0000000..54fb1c3 --- /dev/null +++ b/app/Models/OfferAccessToken.php @@ -0,0 +1,120 @@ + '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; + } +} diff --git a/app/Models/OfferFile.php b/app/Models/OfferFile.php new file mode 100644 index 0000000..be9332c --- /dev/null +++ b/app/Models/OfferFile.php @@ -0,0 +1,99 @@ + '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)]; + } +} diff --git a/app/Models/OfferItem.php b/app/Models/OfferItem.php new file mode 100644 index 0000000..9baf8f8 --- /dev/null +++ b/app/Models/OfferItem.php @@ -0,0 +1,86 @@ + '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); + } +} diff --git a/app/Models/OfferTemplate.php b/app/Models/OfferTemplate.php new file mode 100644 index 0000000..2f49ec6 --- /dev/null +++ b/app/Models/OfferTemplate.php @@ -0,0 +1,77 @@ + '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); + } +} diff --git a/app/Models/OfferVersion.php b/app/Models/OfferVersion.php new file mode 100644 index 0000000..d85b1a5 --- /dev/null +++ b/app/Models/OfferVersion.php @@ -0,0 +1,126 @@ + '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; + } +} diff --git a/app/Repositories/BookingPDFRepository.php b/app/Repositories/BookingPDFRepository.php index dc4d313..e5ba139 100644 --- a/app/Repositories/BookingPDFRepository.php +++ b/app/Repositories/BookingPDFRepository.php @@ -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, diff --git a/app/Repositories/BookingRepository.php b/app/Repositories/BookingRepository.php index 7c4838d..be49e03 100644 --- a/app/Repositories/BookingRepository.php +++ b/app/Repositories/BookingRepository.php @@ -157,11 +157,12 @@ class BookingRepository extends BaseRepository { $this->model = Booking::findOrFail($id); + $this->model->setPriceTotalForCurrentState(); $fill = [ 'deposit_total' => $data['deposit_total'] ? Util::_clean_float($data['deposit_total']) : 0, 'final_payment' => $data['final_payment'] ? Util::_clean_float($data['final_payment']) : 0, 'final_payment_date' => $data['final_payment_date'] ? _reformat_date($data['final_payment_date']) : null, - 'price_total' => ($this->model->getPriceRaw() + $this->model->getServiceTotal(true)), + 'price_total' => $this->model->getPriceTotalRaw(), ]; $this->model->fill($fill); $this->model->save(); @@ -210,7 +211,7 @@ class BookingRepository extends BaseRepository } } } - $this->model->price_total = ($this->model->getPriceRaw() + $this->model->getServiceTotal(true)); + $this->model->setPriceTotalForCurrentState(); $this->model->save(); return $this->model; diff --git a/app/Repositories/CustomerMailRepository.php b/app/Repositories/CustomerMailRepository.php index c1b1226..6397f0c 100644 --- a/app/Repositories/CustomerMailRepository.php +++ b/app/Repositories/CustomerMailRepository.php @@ -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'])){ diff --git a/app/Repositories/LeadRepository.php b/app/Repositories/LeadRepository.php index 929426d..ad0ac87 100644 --- a/app/Repositories/LeadRepository.php +++ b/app/Repositories/LeadRepository.php @@ -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, diff --git a/app/Services/Booking.php b/app/Services/Booking.php index f831388..5ad24aa 100644 --- a/app/Services/Booking.php +++ b/app/Services/Booking.php @@ -15,7 +15,7 @@ class Booking ->pluck('slug', 'id'); } - public static function setOutputDirs(string $dir, string $subdir): void + public static function setOutputDirs(string $dir, ?string $subdir): void { MailDirService::setOutputDir($dir, $subdir); } @@ -39,12 +39,12 @@ class Booking return MailDirService::getCustomerMailDir($id); } - public static function getCustomerMailName(\App\Models\CMSContent $mailDir, int $mailDirId): string + 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 + public static function getCustomerMailEmails(\App\Models\CMSContent $mailDir, ?int $mailDirId): array|string { return MailDirService::getCustomerMailEmails($mailDir, $mailDirId); } diff --git a/app/Services/BookingImport.php b/app/Services/BookingImport.php index acff286..2e6988d 100644 --- a/app/Services/BookingImport.php +++ b/app/Services/BookingImport.php @@ -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, diff --git a/app/Services/Lead.php b/app/Services/Lead.php index e6b47dd..6cbd91b 100644 --- a/app/Services/Lead.php +++ b/app/Services/Lead.php @@ -15,7 +15,7 @@ class Lead ->pluck('slug', 'id'); } - public static function setOutputDirs(string $dir, string $subdir): void + public static function setOutputDirs(string $dir, ?string $subdir): void { MailDirService::setOutputDir($dir, $subdir); } @@ -39,12 +39,12 @@ class Lead return MailDirService::getCustomerMailDir($id); } - public static function getCustomerMailName(\App\Models\CMSContent $mailDir, int $mailDirId): string + 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 + public static function getCustomerMailEmails(\App\Models\CMSContent $mailDir, ?int $mailDirId): array|string { return MailDirService::getCustomerMailEmails($mailDir, $mailDirId); } diff --git a/app/Services/MailDirService.php b/app/Services/MailDirService.php index f36cec6..47b5a35 100644 --- a/app/Services/MailDirService.php +++ b/app/Services/MailDirService.php @@ -11,7 +11,7 @@ class MailDirService { private static array $outputDirs = []; - public static function setOutputDir(string $dir, string $subdir): void + public static function setOutputDir(string $dir, ?string $subdir): void { self::$outputDirs[$dir][] = $subdir; } @@ -29,7 +29,7 @@ class MailDirService return CMSContent::where('identifier', '=', 'customer-mail-dirs')->where('pos', '=', $id)->first(); } - public static function getCustomerMailName(CMSContent $mailDir, int $mailDirId): string + public static function getCustomerMailName(CMSContent $mailDir, ?int $mailDirId): string { $model = self::resolveModel($mailDir, $mailDirId); @@ -47,7 +47,7 @@ class MailDirService /** * @return array|string */ - public static function getCustomerMailEmails(CMSContent $mailDir, int $mailDirId): array|string + public static function getCustomerMailEmails(CMSContent $mailDir, ?int $mailDirId): array|string { $model = self::resolveModel($mailDir, $mailDirId); @@ -79,8 +79,11 @@ class MailDirService return $result; } - private static function resolveModel(CMSContent $mailDir, int $mailDirId): mixed + private static function resolveModel(CMSContent $mailDir, ?int $mailDirId): mixed { + if ($mailDirId === null) { + return null; + } return match ($mailDir->getArrayContent('model')) { 'TravelCountry' => \App\Models\Sym\TravelCountry::find($mailDirId), 'Airline' => Airline::find($mailDirId), diff --git a/bootstrap/cache/config.php b/bootstrap/cache/config.php deleted file mode 100644 index 3ecca50..0000000 --- a/bootstrap/cache/config.php +++ /dev/null @@ -1,1708 +0,0 @@ - - array( - 'name' => 'STERN TOURS CRM', - 'env' => 'local', - 'debug' => true, - 'url' => 'https://mein.sterntours.test', - 'old_url' => 'https://cms-stern-tours.test', - 'url_v2' => 'https://v2.sterntours.test', - 'url_stern' => 'https://sterntours.test', - 'domain_tld' => 'test', - 'timezone' => 'Europe/Berlin', - 'locale' => 'de', - 'fallback_locale' => 'de', - 'key' => 'base64:cxq+xNckU1xLwp8V9Bfj9+nOK5iZL6urcZ1EBO8usXg=', - 'cipher' => 'AES-256-CBC', - 'success_key' => 'f6077389c9ce710e554763a5de02c8ec', - 'providers' => - array( - 0 => 'Illuminate\\Auth\\AuthServiceProvider', - 1 => 'Illuminate\\Broadcasting\\BroadcastServiceProvider', - 2 => 'Illuminate\\Bus\\BusServiceProvider', - 3 => 'Illuminate\\Cache\\CacheServiceProvider', - 4 => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider', - 5 => 'Illuminate\\Cookie\\CookieServiceProvider', - 6 => 'Illuminate\\Database\\DatabaseServiceProvider', - 7 => 'Illuminate\\Encryption\\EncryptionServiceProvider', - 8 => 'Illuminate\\Filesystem\\FilesystemServiceProvider', - 9 => 'Illuminate\\Foundation\\Providers\\FoundationServiceProvider', - 10 => 'Illuminate\\Hashing\\HashServiceProvider', - 11 => 'Illuminate\\Mail\\MailServiceProvider', - 12 => 'Illuminate\\Notifications\\NotificationServiceProvider', - 13 => 'Illuminate\\Pagination\\PaginationServiceProvider', - 14 => 'Illuminate\\Pipeline\\PipelineServiceProvider', - 15 => 'Illuminate\\Queue\\QueueServiceProvider', - 16 => 'Illuminate\\Redis\\RedisServiceProvider', - 17 => 'Illuminate\\Auth\\Passwords\\PasswordResetServiceProvider', - 18 => 'Illuminate\\Session\\SessionServiceProvider', - 19 => 'Illuminate\\Translation\\TranslationServiceProvider', - 20 => 'Illuminate\\Validation\\ValidationServiceProvider', - 21 => 'Illuminate\\View\\ViewServiceProvider', - 22 => 'Laravel\\Tinker\\TinkerServiceProvider', - 23 => 'Laravel\\Passport\\PassportServiceProvider', - 24 => 'App\\Providers\\AppServiceProvider', - 25 => 'App\\Providers\\AuthServiceProvider', - 26 => 'App\\Providers\\EventServiceProvider', - 27 => 'App\\Providers\\RouteServiceProvider', - 28 => 'Barryvdh\\LaravelIdeHelper\\IdeHelperServiceProvider', - 29 => 'Barryvdh\\DomPDF\\ServiceProvider', - 30 => 'Jenssegers\\Date\\DateServiceProvider', - 31 => 'Collective\\Html\\HtmlServiceProvider', - 32 => 'Intervention\\Image\\ImageServiceProvider', - 33 => 'Maatwebsite\\Excel\\ExcelServiceProvider', - 34 => 'Yajra\\DataTables\\DataTablesServiceProvider', - 35 => 'Reliese\\Coders\\CodersServiceProvider', - ), - 'aliases' => - array( - 'App' => 'Illuminate\\Support\\Facades\\App', - 'Arr' => 'Illuminate\\Support\\Arr', - 'Artisan' => 'Illuminate\\Support\\Facades\\Artisan', - 'Auth' => 'Illuminate\\Support\\Facades\\Auth', - 'Blade' => 'Illuminate\\Support\\Facades\\Blade', - 'Broadcast' => 'Illuminate\\Support\\Facades\\Broadcast', - 'Bus' => 'Illuminate\\Support\\Facades\\Bus', - 'Cache' => 'Illuminate\\Support\\Facades\\Cache', - 'Config' => 'Illuminate\\Support\\Facades\\Config', - 'Cookie' => 'Illuminate\\Support\\Facades\\Cookie', - 'Crypt' => 'Illuminate\\Support\\Facades\\Crypt', - 'DB' => 'Illuminate\\Support\\Facades\\DB', - 'Eloquent' => 'Illuminate\\Database\\Eloquent\\Model', - 'Event' => 'Illuminate\\Support\\Facades\\Event', - 'File' => 'Illuminate\\Support\\Facades\\File', - 'Gate' => 'Illuminate\\Support\\Facades\\Gate', - 'Hash' => 'Illuminate\\Support\\Facades\\Hash', - 'Lang' => 'Illuminate\\Support\\Facades\\Lang', - 'Log' => 'Illuminate\\Support\\Facades\\Log', - 'Mail' => 'Illuminate\\Support\\Facades\\Mail', - 'Notification' => 'Illuminate\\Support\\Facades\\Notification', - 'Password' => 'Illuminate\\Support\\Facades\\Password', - 'Queue' => 'Illuminate\\Support\\Facades\\Queue', - 'Redirect' => 'Illuminate\\Support\\Facades\\Redirect', - 'Redis' => 'Illuminate\\Support\\Facades\\Redis', - 'Request' => 'Illuminate\\Support\\Facades\\Request', - 'Response' => 'Illuminate\\Support\\Facades\\Response', - 'Route' => 'Illuminate\\Support\\Facades\\Route', - 'Schema' => 'Illuminate\\Support\\Facades\\Schema', - 'Session' => 'Illuminate\\Support\\Facades\\Session', - 'Storage' => 'Illuminate\\Support\\Facades\\Storage', - 'Str' => 'Illuminate\\Support\\Str', - 'URL' => 'Illuminate\\Support\\Facades\\URL', - 'Validator' => 'Illuminate\\Support\\Facades\\Validator', - 'View' => 'Illuminate\\Support\\Facades\\View', - 'Input' => 'Illuminate\\Support\\Facades\\Request', - 'Form' => 'Collective\\Html\\FormFacade', - 'Image' => 'Intervention\\Image\\Facades\\Image', - 'Carbon' => 'Carbon\\Carbon', - 'Date' => 'Jenssegers\\Date\\Date', - 'HTMLHelper' => 'App\\Helper\\HTMLHelper', - 'Util' => 'App\\Services\\Util', - 'Excel' => 'Maatwebsite\\Excel\\Facades\\Excel', - 'DataTables' => 'Yajra\\DataTables\\Facades\\DataTables', - 'PDF' => 'Barryvdh\\DomPDF\\Facade', - ), - ), - 'auth' => - array( - 'defaults' => - array( - 'guard' => 'web', - 'passwords' => 'users', - ), - 'guards' => - array( - 'web' => - array( - 'driver' => 'session', - 'provider' => 'users', - ), - 'api' => - array( - 'driver' => 'passport', - 'provider' => 'users', - ), - ), - 'providers' => - array( - 'users' => - array( - 'driver' => 'eloquent', - 'model' => 'App\\User', - ), - ), - 'passwords' => - array( - 'users' => - array( - 'provider' => 'users', - 'table' => 'password_resets', - 'expire' => 60, - ), - ), - ), - 'booking' => - array( - 'identifier_general' => 'booking-pdf-g-', - 'identifier_general_name' => 'booking-pdf-general-name', - 'identifier_content' => 'booking-pdf-c-', - 'identifier_content_name' => 'booking-pdf-content-name', - 'max_interval_days' => 28, - 'deposit_percentage_rate' => 25, - 'max_deposit_interval_days' => 28, - 'coupon_default_value' => '50,00', - 'coupon_valid_date_month' => 24, - ), - 'broadcasting' => - array( - 'default' => 'log', - 'connections' => - array( - 'pusher' => - array( - 'driver' => 'pusher', - 'key' => '', - 'secret' => '', - 'app_id' => '', - 'options' => - array( - 'cluster' => 'mt1', - 'encrypted' => true, - ), - ), - 'redis' => - array( - 'driver' => 'redis', - 'connection' => 'default', - ), - 'log' => - array( - 'driver' => 'log', - ), - 'null' => - array( - 'driver' => 'null', - ), - ), - ), - 'cache' => - array( - 'default' => 'file', - 'stores' => - array( - 'apc' => - array( - 'driver' => 'apc', - ), - 'array' => - array( - 'driver' => 'array', - ), - 'database' => - array( - 'driver' => 'database', - 'table' => 'cache', - 'connection' => NULL, - ), - 'file' => - array( - 'driver' => 'file', - 'path' => '/workspace/mein.sterntours.de/storage/framework/cache/data', - ), - 'memcached' => - array( - 'driver' => 'memcached', - 'persistent_id' => NULL, - 'sasl' => - array( - 0 => NULL, - 1 => NULL, - ), - 'options' => - array(), - 'servers' => - array( - 0 => - array( - 'host' => '127.0.0.1', - 'port' => 11211, - 'weight' => 100, - ), - ), - ), - 'redis' => - array( - 'driver' => 'redis', - 'connection' => 'default', - ), - ), - 'prefix' => 'stern_tours_crm_cache', - ), - 'database' => - array( - 'default' => 'mysql', - 'connections' => - array( - 'sqlite' => - array( - 'driver' => 'sqlite', - 'database' => 'stern_crm', - 'prefix' => '', - ), - 'mysql' => - array( - 'driver' => 'mysql', - 'host' => 'global-mysql', - 'port' => '3306', - 'database' => 'stern_crm', - 'username' => 'root', - 'password' => 'password', - 'unix_socket' => '', - 'charset' => 'utf8mb4', - 'collation' => 'utf8mb4_unicode_ci', - 'prefix' => '', - 'strict' => true, - 'engine' => NULL, - ), - 'mysql_stern' => - array( - 'driver' => 'mysql', - 'host' => 'global-mysql', - 'port' => '3306', - 'database' => 'stern_db', - 'username' => 'root', - 'password' => 'password', - 'unix_socket' => '', - 'charset' => 'utf8mb4', - 'collation' => 'utf8mb4_unicode_ci', - 'prefix' => '', - 'strict' => true, - 'engine' => NULL, - ), - 'pgsql' => - array( - 'driver' => 'pgsql', - 'host' => 'global-mysql', - 'port' => '3306', - 'database' => 'stern_crm', - 'username' => 'root', - 'password' => 'password', - 'charset' => 'utf8', - 'prefix' => '', - 'schema' => 'public', - 'sslmode' => 'prefer', - ), - 'sqlsrv' => - array( - 'driver' => 'sqlsrv', - 'host' => 'global-mysql', - 'port' => '3306', - 'database' => 'stern_crm', - 'username' => 'root', - 'password' => 'password', - 'charset' => 'utf8', - 'prefix' => '', - ), - ), - 'migrations' => 'migrations', - 'redis' => - array( - 'client' => 'predis', - 'default' => - array( - 'host' => 'global-redis', - 'password' => NULL, - 'port' => '6379', - 'database' => 0, - ), - ), - ), - 'datatables' => - array( - 'search' => - array( - 'smart' => true, - 'multi_term' => true, - 'case_insensitive' => true, - 'use_wildcards' => false, - ), - 'index_column' => 'DT_Row_Index', - 'engines' => - array( - 'eloquent' => 'Yajra\\DataTables\\EloquentDataTable', - 'query' => 'Yajra\\DataTables\\QueryDataTable', - 'collection' => 'Yajra\\DataTables\\CollectionDataTable', - 'resource' => 'Yajra\\DataTables\\ApiResourceDataTable', - ), - 'builders' => - array(), - 'nulls_last_sql' => '%s %s NULLS LAST', - 'error' => NULL, - 'columns' => - array( - 'excess' => - array( - 0 => 'rn', - 1 => 'row_num', - ), - 'escape' => '*', - 'raw' => - array( - 0 => 'action', - ), - 'blacklist' => - array( - 0 => 'password', - 1 => 'remember_token', - ), - 'whitelist' => '*', - ), - 'json' => - array( - 'header' => - array(), - 'options' => 0, - ), - 'callback' => - array( - 0 => '$', - 1 => '$.', - 2 => 'function', - ), - ), - 'debugbar' => - array( - 'enabled' => NULL, - 'hide_empty_tabs' => true, - 'except' => - array( - 0 => 'telescope*', - ), - 'storage' => - array( - 'enabled' => false, - 'driver' => 'file', - 'path' => '/workspace/mein.sterntours.de/storage/debugbar', - 'connection' => NULL, - 'provider' => '', - ), - 'editor' => 'phpstorm', - 'remote_sites_path' => NULL, - 'local_sites_path' => NULL, - 'include_vendors' => true, - 'capture_ajax' => false, - 'add_ajax_timing' => false, - 'ajax_handler_auto_show' => true, - 'ajax_handler_enable_tab' => true, - 'defer_datasets' => false, - 'error_handler' => false, - 'error_level' => 32767, - 'clockwork' => false, - 'collectors' => - array( - 'phpinfo' => true, - 'messages' => true, - 'time' => true, - 'memory' => true, - 'exceptions' => true, - 'log' => true, - 'db' => true, - 'views' => true, - 'route' => true, - 'auth' => false, - 'gate' => true, - 'session' => true, - 'symfony_request' => true, - 'mail' => true, - 'laravel' => false, - 'events' => false, - 'default_request' => false, - 'logs' => false, - 'files' => false, - 'config' => false, - 'cache' => false, - 'models' => false, - ), - 'options' => - array( - 'auth' => - array( - 'show_name' => true, - ), - 'db' => - array( - 'with_params' => true, - 'backtrace' => true, - 'timeline' => false, - 'explain' => - array( - 'enabled' => false, - 'types' => - array( - 0 => 'SELECT', - ), - ), - 'hints' => true, - ), - 'mail' => - array( - 'full_log' => false, - ), - 'views' => - array( - 'data' => false, - ), - 'route' => - array( - 'label' => true, - ), - 'logs' => - array( - 'file' => NULL, - ), - 'cache' => - array( - 'values' => true, - ), - ), - 'inject' => true, - 'route_prefix' => '_debugbar', - 'route_middleware' => - array(), - 'route_domain' => NULL, - 'theme' => 'auto', - 'debug_backtrace_limit' => 50, - ), - 'dompdf' => - array( - 'show_warnings' => false, - 'public_path' => NULL, - 'convert_entities' => true, - 'options' => - array( - 'font_dir' => '/workspace/mein.sterntours.de/storage/fonts', - 'font_cache' => '/workspace/mein.sterntours.de/storage/fonts', - 'temp_dir' => '/tmp', - 'chroot' => '/workspace/mein.sterntours.de', - 'allowed_protocols' => - array( - 'file://' => - array( - 'rules' => - array(), - ), - 'http://' => - array( - 'rules' => - array(), - ), - 'https://' => - array( - 'rules' => - array(), - ), - ), - 'log_output_file' => NULL, - 'enable_font_subsetting' => false, - 'pdf_backend' => 'CPDF', - 'default_media_type' => 'screen', - 'default_paper_size' => 'a4', - 'default_paper_orientation' => 'portrait', - 'default_font' => 'serif', - 'dpi' => 96, - 'enable_php' => false, - 'enable_javascript' => true, - 'enable_remote' => true, - 'font_height_ratio' => 1.1, - 'enable_html5_parser' => true, - ), - 'orientation' => 'portrait', - 'defines' => - array( - 'font_dir' => '/workspace/mein.sterntours.de/storage/fonts/', - 'font_cache' => '/workspace/mein.sterntours.de/storage/fonts/', - 'temp_dir' => '/tmp', - 'chroot' => '/workspace/mein.sterntours.de', - 'enable_font_subsetting' => false, - 'pdf_backend' => 'CPDF', - 'default_media_type' => 'print', - 'default_paper_size' => 'a4', - 'default_font' => 'sans-serif', - 'dpi' => 300, - 'enable_php' => false, - 'enable_javascript' => true, - 'enable_remote' => true, - 'font_height_ratio' => 0.8, - 'enable_html5_parser' => false, - ), - ), - 'excel' => - array( - 'exports' => - array( - 'chunk_size' => 1000, - 'pre_calculate_formulas' => false, - 'csv' => - array( - 'delimiter' => ',', - 'enclosure' => '"', - 'line_ending' => ' -', - 'use_bom' => false, - 'include_separator_line' => false, - 'excel_compatibility' => false, - ), - ), - 'imports' => - array( - 'read_only' => true, - 'heading_row' => - array( - 'formatter' => 'slug', - ), - 'csv' => - array( - 'delimiter' => ',', - 'enclosure' => '"', - 'escape_character' => '\\', - 'contiguous' => false, - 'input_encoding' => 'UTF-8', - ), - ), - 'extension_detector' => - array( - 'xlsx' => 'Xlsx', - 'xlsm' => 'Xlsx', - 'xltx' => 'Xlsx', - 'xltm' => 'Xlsx', - 'xls' => 'Xls', - 'xlt' => 'Xls', - 'ods' => 'Ods', - 'ots' => 'Ods', - 'slk' => 'Slk', - 'xml' => 'Xml', - 'gnumeric' => 'Gnumeric', - 'htm' => 'Html', - 'html' => 'Html', - 'csv' => 'Csv', - 'tsv' => 'Csv', - 'pdf' => 'Dompdf', - ), - 'value_binder' => - array( - 'default' => 'Maatwebsite\\Excel\\DefaultValueBinder', - ), - 'cache' => - array( - 'driver' => 'memory', - 'batch' => - array( - 'memory_limit' => 60000, - ), - 'illuminate' => - array( - 'store' => NULL, - ), - 'default_ttl' => 10800, - ), - 'transactions' => - array( - 'handler' => 'db', - ), - 'temporary_files' => - array( - 'local_path' => '/tmp', - 'remote_disk' => NULL, - ), - ), - 'fewo' => - array( - 'identifier_content' => 'fewo-pdf-general', - 'identifier_fewo' => 'fewo-pdf-', - ), - 'filesystems' => - array( - 'default' => 'local', - 'cloud' => 's3', - 'disks' => - array( - 'local' => - array( - 'driver' => 'local', - 'root' => '/workspace/mein.sterntours.de/storage/app', - ), - 'public' => - array( - 'driver' => 'local', - 'root' => '/workspace/mein.sterntours.de/storage/app/public', - 'url' => 'https://mein.sterntours.test/storage', - 'visibility' => 'public', - ), - 'customer' => - array( - 'driver' => 'local', - 'root' => '/workspace/mein.sterntours.de/storage/app/customer', - 'url' => 'https://mein.sterntours.test/storage/customer', - 'visibility' => 'public', - ), - 'travel_user' => - array( - 'driver' => 'local', - 'root' => '/workspace/mein.sterntours.de/storage/app/travel_user', - 'url' => 'https://mein.sterntours.test/storage/travel_user', - 'visibility' => 'public', - ), - 'booking' => - array( - 'driver' => 'local', - 'root' => '/workspace/mein.sterntours.de/storage/app/booking', - 'url' => 'https://mein.sterntours.test/storage/booking', - 'visibility' => 'public', - ), - 'general' => - array( - 'driver' => 'local', - 'root' => '/workspace/mein.sterntours.de/storage/app/general', - 'url' => 'https://mein.sterntours.test/storage/general', - 'visibility' => 'public', - ), - 'booking_fewo' => - array( - 'driver' => 'local', - 'root' => '/workspace/mein.sterntours.de/storage/app/booking_fewo', - 'url' => 'https://mein.sterntours.test/storage/booking_fewo', - 'visibility' => 'public', - ), - 'lead' => - array( - 'driver' => 'local', - 'root' => '/workspace/mein.sterntours.de/storage/app/lead', - 'url' => 'https://mein.sterntours.test/storage/lead', - 'visibility' => 'public', - ), - 'fewo_invoices' => - array( - 'driver' => 'local', - 'root' => '/workspace/mein.sterntours.de/storage/app/fewo/invoices', - 'url' => 'https://mein.sterntours.test/storage/fewo/invoices', - 'visibility' => 'public', - ), - 'fewo_infos' => - array( - 'driver' => 'local', - 'root' => '/workspace/mein.sterntours.de/storage/app/fewo/infos', - 'url' => 'https://mein.sterntours.test/storage/fewo/infos', - 'visibility' => 'public', - ), - 's3' => - array( - 'driver' => 's3', - 'key' => NULL, - 'secret' => NULL, - 'region' => NULL, - 'bucket' => NULL, - 'url' => NULL, - ), - ), - ), - 'fpdf' => - array( - 'orientation' => 'P', - 'unit' => 'mm', - 'size' => 'A4', - 'useVaporHeaders' => false, - ), - 'google2fa' => - array( - 'enabled' => true, - 'lifetime' => 0, - 'keep_alive' => true, - 'auth' => 'auth', - 'guard' => '', - 'session_var' => 'google2fa', - 'otp_input' => 'one_time_password', - 'window' => 1, - 'forbid_old_passwords' => false, - 'otp_secret_column' => 'secret_key', - 'view' => 'auth.google2fa', - 'error_messages' => - array( - 'wrong_otp' => 'Das \'One Time Password\' ist falsch.', - 'cannot_be_empty' => 'Das \'One Time Password\' kann nicht leer sein.', - 'unknown' => 'Ein unbekannter Fehler ist aufgetreten. Bitte versuche es erneut.', - ), - 'throw_exceptions' => true, - 'qrcode_image_backend' => 'svg', - ), - 'hashing' => - array( - 'driver' => 'bcrypt', - 'bcrypt' => - array( - 'rounds' => 10, - ), - 'argon' => - array( - 'memory' => 1024, - 'threads' => 2, - 'time' => 2, - ), - ), - 'ide-helper' => - array( - 'filename' => '_ide_helper', - 'models_filename' => '_ide_helper_models.php', - 'meta_filename' => '.phpstorm.meta.php', - 'include_fluent' => false, - 'include_factory_builders' => false, - 'write_model_magic_where' => true, - 'write_model_external_builder_methods' => true, - 'write_model_relation_count_properties' => true, - 'write_eloquent_model_mixins' => false, - 'include_helpers' => false, - 'helper_files' => - array( - 0 => '/workspace/mein.sterntours.de/vendor/laravel/framework/src/Illuminate/Support/helpers.php', - ), - 'model_locations' => - array( - 0 => 'app', - 1 => 'packages', - ), - 'ignored_models' => - array(), - 'model_hooks' => - array(), - 'extra' => - array( - 'Eloquent' => - array( - 0 => 'Illuminate\\Database\\Eloquent\\Builder', - 1 => 'Illuminate\\Database\\Query\\Builder', - ), - 'Session' => - array( - 0 => 'Illuminate\\Session\\Store', - ), - ), - 'magic' => - array( - 'Log' => - array( - 'debug' => 'Monolog\\Logger::addDebug', - 'info' => 'Monolog\\Logger::addInfo', - 'notice' => 'Monolog\\Logger::addNotice', - 'warning' => 'Monolog\\Logger::addWarning', - 'error' => 'Monolog\\Logger::addError', - 'critical' => 'Monolog\\Logger::addCritical', - 'alert' => 'Monolog\\Logger::addAlert', - 'emergency' => 'Monolog\\Logger::addEmergency', - ), - ), - 'interfaces' => - array(), - 'model_camel_case_properties' => false, - 'type_overrides' => - array( - 'integer' => 'int', - 'boolean' => 'bool', - ), - 'include_class_docblocks' => false, - 'force_fqn' => false, - 'use_generics_annotations' => true, - 'additional_relation_types' => - array(), - 'additional_relation_return_types' => - array(), - 'post_migrate' => - array(), - 'format' => 'php', - 'custom_db_types' => - array(), - ), - 'image' => - array( - 'driver' => 'gd', - ), - 'lfm' => - array( - 'use_package_routes' => true, - 'allow_multi_user' => false, - 'allow_share_folder' => false, - 'user_folder_name' => 'IqContent\\LaravelFilemanager\\Handlers\\ConfigHandler', - 'shared_folder_name' => 'shares', - 'thumb_folder_name' => 'thumbs', - 'folder_categories' => - array( - 'file' => - array( - 'folder_name' => 'files', - 'startup_view' => 'grid', - 'max_size' => 50000, - 'valid_mime' => - array( - 0 => 'image/jpeg', - 1 => 'image/pjpeg', - 2 => 'image/png', - 3 => 'image/gif', - 4 => 'image/svg+xml', - 5 => 'application/pdf', - 6 => 'text/plain', - 7 => 'application/msword', - 8 => 'application/vnd.openxmlformats-officedocument.wordprocessingml.document', - 9 => 'application/vnd.ms-word.template.macroEnabled.12', - 10 => 'application/vnd.openxmlformats-officedocument.wordprocessingml.document', - 11 => 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', - 12 => 'application/excel', - ), - ), - 'image' => - array( - 'folder_name' => 'photos', - 'startup_view' => 'list', - 'max_size' => 50000, - 'valid_mime' => - array( - 0 => 'image/jpeg', - 1 => 'image/pjpeg', - 2 => 'image/png', - 3 => 'image/gif', - 4 => 'image/svg+xml', - 5 => 'application/pdf', - 6 => 'text/plain', - ), - ), - ), - 'disk' => 'public', - 'rename_file' => false, - 'alphanumeric_filename' => true, - 'alphanumeric_directory' => true, - 'should_validate_size' => false, - 'should_validate_mime' => false, - 'create_folder_mode' => 493, - 'create_file_mode' => 420, - 'should_change_file_mode' => true, - 'over_write_on_duplicate' => false, - 'should_create_thumbnails' => true, - 'raster_mimetypes' => - array( - 0 => 'image/jpeg', - 1 => 'image/pjpeg', - 2 => 'image/png', - ), - 'thumb_img_width' => 200, - 'thumb_img_height' => 200, - 'default_color' => '#ffc926', - 'resize_aspectRatio' => false, - 'resize_containment' => true, - 'file_type_array' => - array( - 'pdf' => 'Adobe Acrobat', - 'doc' => 'Microsoft Word', - 'docx' => 'Microsoft Word', - 'xls' => 'Microsoft Excel', - 'xlsx' => 'Microsoft Excel', - 'zip' => 'Archive', - 'gif' => 'GIF Image', - 'jpg' => 'JPEG Image', - 'jpeg' => 'JPEG Image', - 'png' => 'PNG Image', - 'ppt' => 'Microsoft PowerPoint', - 'pptx' => 'Microsoft PowerPoint', - ), - 'file_icon_array' => - array( - 'pdf' => 'fa-file-pdf', - 'doc' => 'fa-file-word', - 'docx' => 'fa-file-word', - 'xls' => 'fa-file-excel', - 'xlsx' => 'fa-file-excel', - 'zip' => 'fa-file-archive', - 'gif' => 'fa-file-image', - 'jpg' => 'fa-file-image', - 'jpeg' => 'fa-file-image', - 'png' => 'fa-file-image', - 'ppt' => 'fa-file-powerpoint', - 'pptx' => 'fa-file-powerpoint', - 'mp3' => 'fa-file-audio', - 'mp4' => 'fa-file-video', - 'txt' => 'fa-file-alt', - 'dwg' => 'fa-file-image', - 'youtube' => 'fab fa-youtube-square', - ), - 'php_ini_overrides' => - array( - 'memory_limit' => '256M', - ), - ), - 'localization' => - array( - 'supportedLocales' => - array( - 'de' => - array( - 'name' => 'German', - 'script' => 'Latn', - 'native' => 'Deutsch', - 'regional' => 'de_DE', - ), - ), - ), - 'logging' => - array( - 'default' => 'stack', - 'channels' => - array( - 'stack' => - array( - 'driver' => 'stack', - 'channels' => - array( - 0 => 'single', - ), - ), - 'single' => - array( - 'driver' => 'single', - 'path' => '/workspace/mein.sterntours.de/storage/logs/laravel.log', - 'level' => 'debug', - ), - 'daily' => - array( - 'driver' => 'daily', - 'path' => '/workspace/mein.sterntours.de/storage/logs/laravel.log', - 'level' => 'debug', - 'days' => 7, - ), - 'slack' => - array( - 'driver' => 'slack', - 'url' => NULL, - 'username' => 'Laravel Log', - 'emoji' => ':boom:', - 'level' => 'critical', - ), - 'stderr' => - array( - 'driver' => 'monolog', - 'handler' => 'Monolog\\Handler\\StreamHandler', - 'with' => - array( - 'stream' => 'php://stderr', - ), - ), - 'syslog' => - array( - 'driver' => 'syslog', - 'level' => 'debug', - ), - 'errorlog' => - array( - 'driver' => 'errorlog', - 'level' => 'debug', - ), - 'browser' => - array( - 'driver' => 'single', - 'path' => '/workspace/mein.sterntours.de/storage/logs/browser.log', - 'level' => 'debug', - 'days' => 14, - ), - ), - ), - 'mail' => - array( - 'driver' => 'smtp', - 'host' => 'global-mailpit', - 'port' => '587', - 'from' => - array( - 'address' => 'stern@sterntours.de', - 'name' => 'DEV Reisebüro STERN TOURS', - ), - 'mail_bbc' => - array( - 0 => 'kevin@adametz.media', - ), - 'mail_fewo_employee' => 'kevin@adametz.media', - 'encryption' => 'TLS', - 'username' => 'stern@stern-tours.de', - 'password' => '13C!NlecB!Phil4beAxKl', - 'sendmail' => '/usr/sbin/sendmail -bs', - 'markdown' => - array( - 'theme' => 'default', - 'paths' => - array( - 0 => '/workspace/mein.sterntours.de/resources/views/vendor/mail', - ), - ), - ), - 'models' => - array( - '*' => - array( - 'path' => '/workspace/mein.sterntours.de/app/Models', - 'namespace' => 'App\\Models', - 'parent' => 'Illuminate\\Database\\Eloquent\\Model', - 'use' => - array(), - 'connection' => false, - 'timestamps' => true, - 'soft_deletes' => true, - 'date_format' => 'Y-m-d H:i:s', - 'per_page' => 15, - 'base_files' => false, - 'snake_attributes' => true, - 'qualified_tables' => false, - 'hidden' => - array( - 0 => '*secret*', - 1 => '*password', - 2 => '*token', - ), - 'guarded' => - array(), - 'casts' => - array( - '*_json' => 'json', - ), - 'except' => - array( - 0 => 'migrations', - ), - ), - ), - 'permissions' => - array( - 'groups' => - array( - 0 => - array( - 'my-dat' => - array( - 'name' => 'Ihre Daten', - 'color' => 'client', - ), - ), - 1 => - array( - 'crm' => - array( - 'name' => 'ADMIN CRM ', - 'color' => 'admin', - ), - 'crm-tp' => - array( - 'name' => 'ADMIN CRM > Reiseprogramme', - 'color' => 'admin', - ), - 'crm-tp-pr' => - array( - 'name' => 'ADMIN CRM > Reiseprogramme > Programme', - 'color' => 'admin', - ), - 'crm-tp-dr' => - array( - 'name' => 'ADMIN CRM > Reiseprogramme > Vorlagen', - 'color' => 'admin', - ), - 'crm-tp-tc' => - array( - 'name' => 'ADMIN CRM > Reiseprogramme > Inhalte', - 'color' => 'admin', - ), - 'crm-bo' => - array( - 'name' => 'ADMIN CRM > Buchungen', - 'color' => 'admin', - ), - 'crm-bo-re' => - array( - 'name' => 'ADMIN CRM > Buchungen > Übersicht', - 'color' => 'admin', - ), - 'crm-bo-bo' => - array( - 'name' => 'ADMIN CRM > Buchungen > Buchungen', - 'color' => 'admin', - ), - 'crm-bo-le' => - array( - 'name' => 'ADMIN CRM > Buchungen > Anfragen', - 'color' => 'admin', - ), - 'crm-bo-cu' => - array( - 'name' => 'ADMIN CRM > Buchungen > Kunden', - 'color' => 'admin', - ), - 'crm-cm' => - array( - 'name' => 'ADMIN CRM > Kundenverwaltung', - 'color' => 'admin', - ), - 'crm-cm-cf' => - array( - 'name' => 'ADMIN CRM > Kundenverwaltung > Kunden (FeWo)', - 'color' => 'admin', - ), - 'crm-cm-bf' => - array( - 'name' => 'ADMIN CRM > Kundenverwaltung > Buchungen (FeWo)', - 'color' => 'admin', - ), - 'crm-mail' => - array( - 'name' => 'ADMIN CRM > E-Mails', - 'color' => 'admin', - ), - 'crm-mail-le' => - array( - 'name' => 'ADMIN CRM > E-Mails > Anfragen', - 'color' => 'admin', - ), - 'crm-mail-bo' => - array( - 'name' => 'ADMIN CRM > E-Mails > Buchungen', - 'color' => 'admin', - ), - 'crm-mail-bf' => - array( - 'name' => 'ADMIN CRM > E-Mails > Buchungen (Fewo)', - 'color' => 'admin', - ), - 'crm-iq-tl' => - array( - 'name' => 'ADMIN CRM > Reisebausteine', - 'color' => 'admin', - ), - 'crm-iq-tl-pro' => - array( - 'name' => 'ADMIN CRM > Reisebausteine > Programm', - 'color' => 'admin', - ), - 'crm-iq-tl-gp' => - array( - 'name' => 'ADMIN CRM > Reisebausteine > Gruppe', - 'color' => 'admin', - ), - 'crm-iq-tl-it' => - array( - 'name' => 'ADMIN CRM > Reisebausteine > Baustein', - 'color' => 'admin', - ), - 'crm-old-cm' => - array( - 'name' => 'ADMIN CRM altes System > Kundenverwaltung', - 'color' => 'info', - ), - 'cms' => - array( - 'name' => 'ADMIN CMS', - 'color' => 'secondary', - ), - 'cms-iq-assets' => - array( - 'name' => 'ADMIN CMS > Medien', - 'color' => 'secondary', - ), - 'cms-tg' => - array( - 'name' => 'ADMIN CMS > Reiseführer', - 'color' => 'secondary', - ), - 'cms-fewo' => - array( - 'name' => 'ADMIN CMS > FeWo', - 'color' => 'secondary', - ), - 'cms-book' => - array( - 'name' => 'ADMIN CMS > Buchungen', - 'color' => 'secondary', - ), - 'cms-fb' => - array( - 'name' => 'ADMIN CMS > Feedback', - 'color' => 'secondary', - ), - 'cms-nw' => - array( - 'name' => 'ADMIN CMS > News', - 'color' => 'secondary', - ), - 'cms-aq' => - array( - 'name' => 'ADMIN CMS > Fragen & Antworten', - 'color' => 'secondary', - ), - 'cms-sb' => - array( - 'name' => 'ADMIN CMS > Sidebar', - 'color' => 'secondary', - ), - 'cms-cn' => - array( - 'name' => 'ADMIN CMS > Inhalte', - 'color' => 'secondary', - ), - 'cms-cn-in' => - array( - 'name' => 'ADMIN CMS > Inhalte > Infos', - 'color' => 'secondary', - ), - 'cms-cn-al' => - array( - 'name' => 'ADMIN CMS > Inhalte > Inhalte', - 'color' => 'secondary', - ), - 'cms-cn-au' => - array( - 'name' => 'ADMIN CMS > Inhalte > Autor', - 'color' => 'secondary', - ), - 'cms-cn-co' => - array( - 'name' => 'ADMIN CMS > Inhalte > Länder', - 'color' => 'secondary', - ), - 'cms-newsletter' => - array( - 'name' => 'ADMIN CMS > Newsletter', - 'color' => 'secondary', - ), - 'crm-nav-api' => - array( - 'name' => 'ADMIN CRM > Navigation API', - 'color' => 'secondary', - ), - ), - 2 => - array( - 'sua-bo-n-edit' => - array( - 'name' => 'SUPERADMIN > Buchungen > Notizen > bearbeiten', - 'color' => 'secondary', - ), - 'sua-fewo-n-edit' => - array( - 'name' => 'SUPERADMIN > FeWo > Notizen > bearbeiten', - 'color' => 'secondary', - ), - 'sua-st' => - array( - 'name' => 'SUPERADMIN > Einstellungen', - 'color' => 'superadmin', - ), - 'sua-st-al' => - array( - 'name' => 'SUPERADMIN > Einstellungen > Airline', - 'color' => 'superadmin', - ), - 'sua-st-ap' => - array( - 'name' => 'SUPERADMIN > Einstellungen > Airport', - 'color' => 'superadmin', - ), - 'sua-st-em' => - array( - 'name' => 'SUPERADMIN > Einstellungen > E-Mails', - 'color' => 'superadmin', - ), - 'sua-st-ke' => - array( - 'name' => 'SUPERADMIN > Einstellungen > Keywords', - 'color' => 'superadmin', - ), - 'sua-st-sp' => - array( - 'name' => 'SUPERADMIN > Einstellungen > Leistungsträger', - 'color' => 'superadmin', - ), - 'sua-st-tn' => - array( - 'name' => 'SUPERADMIN > Einstellungen > Nationalitäten', - 'color' => 'superadmin', - ), - 'sua-st-co' => - array( - 'name' => 'SUPERADMIN > Einstellungen > Reiseländer', - 'color' => 'superadmin', - ), - 'sua-st-tp' => - array( - 'name' => 'SUPERADMIN > Einstellungen > Reiseprogramme', - 'color' => 'superadmin', - ), - 'sua-st-tpl' => - array( - 'name' => 'SUPERADMIN > Einstellungen > Reiseorte', - 'color' => 'superadmin', - ), - 'sua-st-bs' => - array( - 'name' => 'SUPERADMIN > Einstellungen > Reisestatus', - 'color' => 'superadmin', - ), - 'sua-st-tc' => - array( - 'name' => 'SUPERADMIN > Einstellungen > Veranstalter', - 'color' => 'superadmin', - ), - 'sua-st-tca' => - array( - 'name' => 'SUPERADMIN > Einstellungen > Reiseart', - 'color' => 'superadmin', - ), - 'sua-st-tap' => - array( - 'name' => 'SUPERADMIN > Einstellungen > Zielflughafen', - 'color' => 'superadmin', - ), - 'sua-st-in' => - array( - 'name' => 'SUPERADMIN > Einstellungen > Versicherungen', - 'color' => 'superadmin', - ), - 'sua-st-ca' => - array( - 'name' => 'SUPERADMIN > Einstellungen > Kategorien', - 'color' => 'superadmin', - ), - 'sua-st-tgn' => - array( - 'name' => 'SUPERADMIN > Einstellungen > Reisehinweise', - 'color' => 'superadmin', - ), - 'sua-re' => - array( - 'name' => 'SUPERADMIN > Export', - 'color' => 'superadmin', - ), - 'sua-re-bo' => - array( - 'name' => 'SUPERADMIN > Export > Buchungen', - 'color' => 'superadmin', - ), - 'sua-re-pp' => - array( - 'name' => 'SUPERADMIN > Export > Leistungsträger', - 'color' => 'superadmin', - ), - 'sua-re-fw' => - array( - 'name' => 'SUPERADMIN > Export > Fewo', - 'color' => 'superadmin', - ), - 'sua-re-le' => - array( - 'name' => 'SUPERADMIN > Export > Anfragen', - 'color' => 'superadmin', - ), - 'sua-ur-rt' => - array( - 'name' => 'SUPERADMIN > User Rechte', - 'color' => 'danger', - ), - ), - ), - 'roles' => - array( - 0 => 'Kunde', - 1 => 'Admin', - 2 => 'SuperAdmin', - ), - ), - 'queue' => - array( - 'default' => 'sync', - 'connections' => - array( - 'sync' => - array( - 'driver' => 'sync', - ), - 'database' => - array( - 'driver' => 'database', - 'table' => 'jobs', - 'queue' => 'default', - 'retry_after' => 90, - ), - 'beanstalkd' => - array( - 'driver' => 'beanstalkd', - 'host' => 'localhost', - 'queue' => 'default', - 'retry_after' => 90, - ), - 'sqs' => - array( - 'driver' => 'sqs', - 'key' => 'your-public-key', - 'secret' => 'your-secret-key', - 'prefix' => 'https://sqs.us-east-1.amazonaws.com/your-account-id', - 'queue' => 'your-queue-name', - 'region' => 'us-east-1', - ), - 'redis' => - array( - 'driver' => 'redis', - 'connection' => 'default', - 'queue' => 'default', - 'retry_after' => 90, - 'block_for' => NULL, - ), - ), - 'failed' => - array( - 'database' => 'mysql', - 'table' => 'failed_jobs', - ), - ), - 'services' => - array( - 'mailgun' => - array( - 'domain' => NULL, - 'secret' => NULL, - ), - 'ses' => - array( - 'key' => NULL, - 'secret' => NULL, - 'region' => 'us-east-1', - ), - 'sparkpost' => - array( - 'secret' => NULL, - ), - 'stripe' => - array( - 'model' => 'App\\User', - 'key' => NULL, - 'secret' => NULL, - ), - ), - 'session' => - array( - 'driver' => 'file', - 'lifetime' => '120', - 'expire_on_close' => false, - 'encrypt' => false, - 'files' => '/workspace/mein.sterntours.de/storage/framework/sessions', - 'connection' => NULL, - 'table' => 'sessions', - 'store' => NULL, - 'lottery' => - array( - 0 => 2, - 1 => 100, - ), - 'cookie' => 'stern_tours_crm_session', - 'path' => '/', - 'domain' => NULL, - 'secure' => false, - 'http_only' => true, - 'same_site' => NULL, - ), - 'tinker' => - array( - 'commands' => - array(), - 'alias' => - array(), - 'dont_alias' => - array(), - 'trust_project' => 'always', - ), - 'trustedproxy' => - array( - 'proxies' => NULL, - 'headers' => 62, - ), - 'view' => - array( - 'paths' => - array( - 0 => '/workspace/mein.sterntours.de/resources/views', - ), - 'compiled' => '/workspace/mein.sterntours.de/storage/framework/views', - ), - 'cart' => - array( - 'tax' => 10, - 'database' => - array( - 'connection' => NULL, - 'table' => 'shoppingcart', - ), - 'destroy_on_logout' => false, - 'format' => - array( - 'decimals' => 2, - 'decimal_point' => '.', - 'thousand_seperator' => '', - ), - 'discountOnFees' => false, - ), - 'boost' => - array( - 'enabled' => true, - 'browser_logs_watcher' => true, - 'executable_paths' => - array( - 'php' => NULL, - 'composer' => NULL, - 'npm' => NULL, - 'vendor_bin' => NULL, - ), - ), - 'mcp' => - array( - 'redirect_domains' => - array( - 0 => '*', - ), - ), - 'passport' => - array( - 'guard' => 'web', - 'private_key' => NULL, - 'public_key' => NULL, - 'client_uuids' => false, - 'personal_access_client' => - array( - 'id' => NULL, - 'secret' => NULL, - ), - ), - 'flare' => - array( - 'key' => NULL, - 'flare_middleware' => - array( - 0 => 'Spatie\\FlareClient\\FlareMiddleware\\RemoveRequestIp', - 1 => 'Spatie\\FlareClient\\FlareMiddleware\\AddGitInformation', - 2 => 'Spatie\\LaravelIgnition\\FlareMiddleware\\AddNotifierName', - 3 => 'Spatie\\LaravelIgnition\\FlareMiddleware\\AddEnvironmentInformation', - 4 => 'Spatie\\LaravelIgnition\\FlareMiddleware\\AddExceptionInformation', - 5 => 'Spatie\\LaravelIgnition\\FlareMiddleware\\AddDumps', - 'Spatie\\LaravelIgnition\\FlareMiddleware\\AddLogs' => - array( - 'maximum_number_of_collected_logs' => 200, - ), - 'Spatie\\LaravelIgnition\\FlareMiddleware\\AddQueries' => - array( - 'maximum_number_of_collected_queries' => 200, - 'report_query_bindings' => true, - ), - 'Spatie\\LaravelIgnition\\FlareMiddleware\\AddJobs' => - array( - 'max_chained_job_reporting_depth' => 5, - ), - 6 => 'Spatie\\LaravelIgnition\\FlareMiddleware\\AddContext', - 7 => 'Spatie\\LaravelIgnition\\FlareMiddleware\\AddExceptionHandledStatus', - 'Spatie\\FlareClient\\FlareMiddleware\\CensorRequestBodyFields' => - array( - 'censor_fields' => - array( - 0 => 'password', - 1 => 'password_confirmation', - ), - ), - 'Spatie\\FlareClient\\FlareMiddleware\\CensorRequestHeaders' => - array( - 'headers' => - array( - 0 => 'API-KEY', - 1 => 'Authorization', - 2 => 'Cookie', - 3 => 'Set-Cookie', - 4 => 'X-CSRF-TOKEN', - 5 => 'X-XSRF-TOKEN', - ), - ), - ), - 'send_logs_as_events' => true, - ), - 'ignition' => - array( - 'editor' => 'phpstorm', - 'theme' => 'auto', - 'enable_share_button' => true, - 'register_commands' => false, - 'solution_providers' => - array( - 0 => 'Spatie\\Ignition\\Solutions\\SolutionProviders\\BadMethodCallSolutionProvider', - 1 => 'Spatie\\Ignition\\Solutions\\SolutionProviders\\MergeConflictSolutionProvider', - 2 => 'Spatie\\Ignition\\Solutions\\SolutionProviders\\UndefinedPropertySolutionProvider', - 3 => 'Spatie\\LaravelIgnition\\Solutions\\SolutionProviders\\IncorrectValetDbCredentialsSolutionProvider', - 4 => 'Spatie\\LaravelIgnition\\Solutions\\SolutionProviders\\MissingAppKeySolutionProvider', - 5 => 'Spatie\\LaravelIgnition\\Solutions\\SolutionProviders\\DefaultDbNameSolutionProvider', - 6 => 'Spatie\\LaravelIgnition\\Solutions\\SolutionProviders\\TableNotFoundSolutionProvider', - 7 => 'Spatie\\LaravelIgnition\\Solutions\\SolutionProviders\\MissingImportSolutionProvider', - 8 => 'Spatie\\LaravelIgnition\\Solutions\\SolutionProviders\\InvalidRouteActionSolutionProvider', - 9 => 'Spatie\\LaravelIgnition\\Solutions\\SolutionProviders\\ViewNotFoundSolutionProvider', - 10 => 'Spatie\\LaravelIgnition\\Solutions\\SolutionProviders\\RunningLaravelDuskInProductionProvider', - 11 => 'Spatie\\LaravelIgnition\\Solutions\\SolutionProviders\\MissingColumnSolutionProvider', - 12 => 'Spatie\\LaravelIgnition\\Solutions\\SolutionProviders\\UnknownValidationSolutionProvider', - 13 => 'Spatie\\LaravelIgnition\\Solutions\\SolutionProviders\\MissingMixManifestSolutionProvider', - 14 => 'Spatie\\LaravelIgnition\\Solutions\\SolutionProviders\\MissingViteManifestSolutionProvider', - 15 => 'Spatie\\LaravelIgnition\\Solutions\\SolutionProviders\\MissingLivewireComponentSolutionProvider', - 16 => 'Spatie\\LaravelIgnition\\Solutions\\SolutionProviders\\UndefinedViewVariableSolutionProvider', - 17 => 'Spatie\\LaravelIgnition\\Solutions\\SolutionProviders\\GenericLaravelExceptionSolutionProvider', - 18 => 'Spatie\\LaravelIgnition\\Solutions\\SolutionProviders\\OpenAiSolutionProvider', - 19 => 'Spatie\\LaravelIgnition\\Solutions\\SolutionProviders\\SailNetworkSolutionProvider', - 20 => 'Spatie\\LaravelIgnition\\Solutions\\SolutionProviders\\UnknownMysql8CollationSolutionProvider', - 21 => 'Spatie\\LaravelIgnition\\Solutions\\SolutionProviders\\UnknownMariadbCollationSolutionProvider', - ), - 'ignored_solution_providers' => - array(), - 'enable_runnable_solutions' => NULL, - 'remote_sites_path' => '/workspace/mein.sterntours.de', - 'local_sites_path' => '', - 'housekeeping_endpoint_prefix' => '_ignition', - 'settings_file_path' => '', - 'recorders' => - array( - 0 => 'Spatie\\LaravelIgnition\\Recorders\\DumpRecorder\\DumpRecorder', - 1 => 'Spatie\\LaravelIgnition\\Recorders\\JobRecorder\\JobRecorder', - 2 => 'Spatie\\LaravelIgnition\\Recorders\\LogRecorder\\LogRecorder', - 3 => 'Spatie\\LaravelIgnition\\Recorders\\QueryRecorder\\QueryRecorder', - ), - 'open_ai_key' => NULL, - 'with_stack_frame_arguments' => true, - 'argument_reducers' => - array( - 0 => 'Spatie\\Backtrace\\Arguments\\Reducers\\BaseTypeArgumentReducer', - 1 => 'Spatie\\Backtrace\\Arguments\\Reducers\\ArrayArgumentReducer', - 2 => 'Spatie\\Backtrace\\Arguments\\Reducers\\StdClassArgumentReducer', - 3 => 'Spatie\\Backtrace\\Arguments\\Reducers\\EnumArgumentReducer', - 4 => 'Spatie\\Backtrace\\Arguments\\Reducers\\ClosureArgumentReducer', - 5 => 'Spatie\\Backtrace\\Arguments\\Reducers\\DateTimeArgumentReducer', - 6 => 'Spatie\\Backtrace\\Arguments\\Reducers\\DateTimeZoneArgumentReducer', - 7 => 'Spatie\\Backtrace\\Arguments\\Reducers\\SymphonyRequestArgumentReducer', - 8 => 'Spatie\\LaravelIgnition\\ArgumentReducers\\ModelArgumentReducer', - 9 => 'Spatie\\LaravelIgnition\\ArgumentReducers\\CollectionArgumentReducer', - 10 => 'Spatie\\Backtrace\\Arguments\\Reducers\\StringableArgumentReducer', - ), - ), - 'sluggable' => - array( - 'source' => NULL, - 'maxLength' => NULL, - 'maxLengthKeepWords' => true, - 'method' => NULL, - 'separator' => '-', - 'unique' => true, - 'uniqueSuffix' => NULL, - 'firstUniqueSuffix' => 2, - 'includeTrashed' => false, - 'reserved' => NULL, - 'onUpdate' => false, - 'slugEngineOptions' => - array(), - ), -); diff --git a/config/filesystems.php b/config/filesystems.php index 2318ca2..97762c8 100755 --- a/config/filesystems.php +++ b/config/filesystems.php @@ -72,6 +72,12 @@ return [ 'url' => env('APP_URL').'/storage/booking', 'visibility' => 'public', ], + 'offer' => [ + 'driver' => 'local', + 'root' => storage_path('app/offer'), + 'url' => env('APP_URL').'/storage/offer', + 'visibility' => 'public', + ], 'general' => [ 'driver' => 'local', 'root' => storage_path('app/general'), diff --git a/database/migrations/2025_04_15_200001_phase2_rename_customer_to_contacts.php b/database/migrations/2025_04_15_200001_phase2_rename_customer_to_contacts.php new file mode 100644 index 0000000..17cd0b4 --- /dev/null +++ b/database/migrations/2025_04_15_200001_phase2_rename_customer_to_contacts.php @@ -0,0 +1,30 @@ +dropForeign('booking_lead_id_lead_id'); + }); + + DB::statement('ALTER TABLE `booking` RENAME COLUMN `lead_id` TO `inquiry_id`'); + + Schema::table('booking', function (Blueprint $table) { + // Neuen FK auf umbenannte Tabelle setzen + $table->foreign('inquiry_id', 'booking_inquiry_id_inquiries_id') + ->references('id')->on('inquiries') + ->onDelete('no action') + ->onUpdate('no action'); + }); + } + + public function down(): void + { + Schema::table('booking', function (Blueprint $table) { + $table->dropForeign('booking_inquiry_id_inquiries_id'); + }); + + DB::statement('ALTER TABLE `booking` RENAME COLUMN `inquiry_id` TO `lead_id`'); + + Schema::table('booking', function (Blueprint $table) { + $table->foreign('lead_id', 'booking_lead_id_lead_id') + ->references('id')->on('lead') + ->onDelete('no action') + ->onUpdate('no action'); + }); + } +}; diff --git a/database/migrations/2025_04_15_300001_phase3_create_participants_unified_table.php b/database/migrations/2025_04_15_300001_phase3_create_participants_unified_table.php new file mode 100644 index 0000000..70a1c2f --- /dev/null +++ b/database/migrations/2025_04_15_300001_phase3_create_participants_unified_table.php @@ -0,0 +1,123 @@ +id(); + + // Kontext-FKs — genau einer ist gesetzt, der andere NULL + $table->unsignedBigInteger('inquiry_id')->nullable(); + $table->unsignedBigInteger('booking_id')->nullable(); + + // Teilnehmerdaten + $table->string('participant_name')->nullable(); + $table->string('participant_firstname')->nullable(); + $table->date('participant_birthdate')->nullable(); + $table->unsignedBigInteger('participant_salutation_id')->nullable(); + $table->boolean('participant_child')->default(false); + $table->integer('nationality_id')->nullable(); + + // Nur bei Buchungs-Teilnehmern + $table->boolean('participant_pass')->default(false); + $table->boolean('participant_storno')->default(false); + + // Markiert den Hauptreisenden aus lead.participant_name + $table->boolean('is_lead_contact')->default(false); + + $table->timestamps(); + + $table->index('inquiry_id'); + $table->index('booking_id'); + + $table->foreign('inquiry_id', 'pu_inquiry_id_fk') + ->references('id')->on('inquiries') + ->onDelete('cascade'); + + $table->foreign('booking_id', 'pu_booking_id_fk') + ->references('id')->on('booking') + ->onDelete('cascade'); + + $table->foreign('participant_salutation_id', 'pu_salutation_id_fk') + ->references('id')->on('salutation') + ->onDelete('set null'); + }); + + // ── Daten migrieren ───────────────────────────────────────────────── + + // 1. Aus lead_participant → inquiry_id gesetzt + DB::statement(" + INSERT INTO participants_unified + (inquiry_id, booking_id, participant_name, participant_firstname, + participant_birthdate, participant_salutation_id, participant_child, + nationality_id, is_lead_contact, created_at, updated_at) + SELECT + lead_id, NULL, + participant_name, participant_firstname, + participant_birthdate, participant_salutation_id, participant_child, + nationality_id, 0, + NOW(), NOW() + FROM lead_participant + "); + + // 2. Aus lead.participant_name → Hauptreisende-Datensätze (is_lead_contact = 1) + DB::statement(" + INSERT INTO participants_unified + (inquiry_id, booking_id, participant_name, participant_firstname, + participant_birthdate, participant_salutation_id, participant_child, + nationality_id, is_lead_contact, created_at, updated_at) + SELECT + id, NULL, + participant_name, participant_firstname, + participant_birthdate, participant_salutation_id, 0, + NULL, 1, + NOW(), NOW() + FROM inquiries + WHERE participant_name IS NOT NULL + AND participant_name != '' + "); + + // 3. Aus participant → booking_id gesetzt + DB::statement(" + INSERT INTO participants_unified + (inquiry_id, booking_id, participant_name, participant_firstname, + participant_birthdate, participant_salutation_id, participant_child, + nationality_id, participant_pass, participant_storno, + is_lead_contact, created_at, updated_at) + SELECT + NULL, booking_id, + participant_name, participant_firstname, + participant_birthdate, participant_salutation_id, participant_child, + nationality_id, participant_pass, participant_storno, + 0, NOW(), NOW() + FROM participant + "); + } + + public function down(): void + { + Schema::dropIfExists('participants_unified'); + } +}; diff --git a/database/migrations/2025_04_15_300002_phase3_drop_old_participant_tables.php b/database/migrations/2025_04_15_300002_phase3_drop_old_participant_tables.php new file mode 100644 index 0000000..3f2cbec --- /dev/null +++ b/database/migrations/2025_04_15_300002_phase3_drop_old_participant_tables.php @@ -0,0 +1,40 @@ +id(); + + // Kontext-FKs — bei customer_mails können beide gesetzt sein + $table->unsignedBigInteger('inquiry_id')->nullable(); + $table->unsignedBigInteger('booking_id')->nullable(); + $table->unsignedBigInteger('contact_id')->nullable(); + + // Reply-Chain — kein FK-Constraint (cross-table Remapping) + $table->unsignedBigInteger('reply_id')->nullable(); + + // Herkunft für Remapping und spätere Analyse + $table->enum('legacy_source', ['lead_mail', 'customer_mail']); + $table->unsignedBigInteger('legacy_id'); + + // Mail-Felder (identisch in beiden Quell-Tabellen) + $table->boolean('is_answer')->default(false); + $table->string('email')->nullable(); + $table->text('recipient')->nullable(); + $table->text('cc')->nullable(); + $table->text('bcc')->nullable(); + $table->string('subject')->nullable(); + $table->text('message')->nullable(); + $table->boolean('dir')->default(false); + $table->unsignedBigInteger('subdir')->nullable(); + + // Nur in customer_mails vorhanden + $table->unsignedBigInteger('travel_country_id')->nullable(); + + $table->boolean('draft')->default(false); + $table->boolean('important')->default(false); + $table->boolean('send')->default(false); + $table->boolean('fail')->default(false); + $table->text('error')->nullable(); + $table->text('forward')->nullable(); + + $table->dateTime('sent_at')->nullable(); + $table->dateTime('scheduled_at')->nullable(); + $table->dateTime('delivered_at')->nullable(); + + $table->timestamps(); + + $table->index('inquiry_id'); + $table->index('booking_id'); + $table->index('contact_id'); + $table->index('reply_id'); + $table->index(['legacy_source', 'legacy_id'], 'comm_legacy_idx'); + + $table->foreign('inquiry_id', 'comm_inquiry_id_fk') + ->references('id')->on('inquiries') + ->onDelete('cascade'); + + $table->foreign('booking_id', 'comm_booking_id_fk') + ->references('id')->on('booking') + ->onDelete('cascade'); + + $table->foreign('contact_id', 'comm_contact_id_fk') + ->references('id')->on('contacts') + ->onDelete('set null'); + }); + + // ── Daten migrieren ───────────────────────────────────────────────── + + // 1. Aus lead_mails → inquiry_id gesetzt + DB::statement(" + INSERT INTO communications + (inquiry_id, booking_id, contact_id, is_answer, reply_id, + legacy_source, legacy_id, + email, recipient, cc, bcc, subject, message, + dir, subdir, travel_country_id, + draft, important, send, fail, error, forward, + sent_at, scheduled_at, delivered_at, created_at, updated_at) + SELECT + lead_id, NULL, customer_id, is_answer, reply_id, + 'lead_mail', id, + email, recipient, cc, bcc, subject, message, + dir, subdir, NULL, + draft, important, send, fail, error, forward, + sent_at, scheduled_at, delivered_at, created_at, updated_at + FROM lead_mails + "); + + // 2. Aus customer_mails → booking_id gesetzt (kann zusätzlich lead_id haben) + DB::statement(" + INSERT INTO communications + (inquiry_id, booking_id, contact_id, is_answer, reply_id, + legacy_source, legacy_id, + email, recipient, cc, bcc, subject, message, + dir, subdir, travel_country_id, + draft, important, send, fail, error, forward, + sent_at, scheduled_at, delivered_at, created_at, updated_at) + SELECT + lead_id, booking_id, customer_id, is_answer, reply_id, + 'customer_mail', id, + email, recipient, cc, bcc, subject, message, + dir, subdir, travel_country_id, + draft, important, send, fail, error, forward, + sent_at, scheduled_at, delivered_at, created_at, updated_at + FROM customer_mails + "); + + // 3. Reply-IDs remappen: alte source-table-IDs auf neue communications-IDs umstellen + // Funktioniert weil reply_id immer auf eine Mail derselben Quell-Tabelle zeigt. + DB::statement(" + UPDATE communications c1 + INNER JOIN communications c2 + ON c1.legacy_source = c2.legacy_source + AND c2.legacy_id = c1.reply_id + SET c1.reply_id = c2.id + WHERE c1.reply_id IS NOT NULL + "); + } + + public function down(): void + { + Schema::dropIfExists('communications'); + } +}; diff --git a/database/migrations/2025_04_15_400002_phase4_create_notices_table.php b/database/migrations/2025_04_15_400002_phase4_create_notices_table.php new file mode 100644 index 0000000..624c891 --- /dev/null +++ b/database/migrations/2025_04_15_400002_phase4_create_notices_table.php @@ -0,0 +1,94 @@ +id(); + + // Kontext-FKs — genau einer ist gesetzt + $table->unsignedBigInteger('inquiry_id')->nullable(); + $table->unsignedBigInteger('booking_id')->nullable(); + + // Benutzer-Referenzen (integer wie in den Quell-Tabellen) + $table->unsignedInteger('from_user_id'); + $table->unsignedInteger('to_user_id')->nullable(); + + $table->text('message')->nullable(); + $table->boolean('show')->default(false); + $table->boolean('important')->default(false); + $table->dateTime('edit_at')->nullable(); + + $table->timestamps(); + + $table->index('inquiry_id'); + $table->index('booking_id'); + $table->index('from_user_id'); + $table->index('to_user_id'); + + $table->foreign('inquiry_id', 'notices_inquiry_id_fk') + ->references('id')->on('inquiries') + ->onDelete('cascade'); + + $table->foreign('booking_id', 'notices_booking_id_fk') + ->references('id')->on('booking') + ->onDelete('cascade'); + + $table->foreign('from_user_id', 'notices_from_user_id_fk') + ->references('id')->on('users') + ->onDelete('cascade'); + + $table->foreign('to_user_id', 'notices_to_user_id_fk') + ->references('id')->on('users') + ->onDelete('cascade'); + }); + + // ── Daten migrieren ───────────────────────────────────────────────── + + // 1. Aus lead_notices → inquiry_id gesetzt + DB::statement(" + INSERT INTO notices + (inquiry_id, booking_id, from_user_id, to_user_id, + message, show, important, edit_at, created_at, updated_at) + SELECT + lead_id, NULL, from_user_id, to_user_id, + message, show, important, edit_at, created_at, updated_at + FROM lead_notices + "); + + // 2. Aus booking_notices → booking_id gesetzt + DB::statement(" + INSERT INTO notices + (inquiry_id, booking_id, from_user_id, to_user_id, + message, show, important, edit_at, created_at, updated_at) + SELECT + NULL, booking_id, from_user_id, to_user_id, + message, show, important, edit_at, created_at, updated_at + FROM booking_notices + "); + } + + public function down(): void + { + Schema::dropIfExists('notices'); + } +}; diff --git a/database/migrations/2025_04_15_400003_phase4_create_attachments_table.php b/database/migrations/2025_04_15_400003_phase4_create_attachments_table.php new file mode 100644 index 0000000..7f44feb --- /dev/null +++ b/database/migrations/2025_04_15_400003_phase4_create_attachments_table.php @@ -0,0 +1,118 @@ +id(); + + // Kontext-FKs + $table->unsignedBigInteger('inquiry_id')->nullable(); + $table->unsignedBigInteger('booking_id')->nullable(); + + // Verknüpfung zur dazugehörigen Mail (nur bei Anfrage-Anhängen) + $table->unsignedBigInteger('communication_id')->nullable(); + + // Datei-Metadaten + $table->string('identifier')->nullable(); + $table->string('filename'); + $table->string('dir')->nullable(); + $table->string('original_name')->nullable(); + $table->string('ext')->nullable(); + $table->string('mime_type')->nullable(); + $table->unsignedInteger('size')->nullable(); + + // Herkunft für spätere Analyse / Rollback + $table->enum('legacy_source', ['lead_file', 'booking_file']); + $table->unsignedBigInteger('legacy_id'); + + $table->timestamps(); + + $table->index('inquiry_id'); + $table->index('booking_id'); + $table->index('communication_id'); + $table->index('identifier', 'attachments_identifier_idx'); + $table->index(['legacy_source', 'legacy_id'], 'attachments_legacy_idx'); + + $table->foreign('inquiry_id', 'att_inquiry_id_fk') + ->references('id')->on('inquiries') + ->onDelete('cascade'); + + $table->foreign('booking_id', 'att_booking_id_fk') + ->references('id')->on('booking') + ->onDelete('cascade'); + + $table->foreign('communication_id', 'att_communication_id_fk') + ->references('id')->on('communications') + ->onDelete('set null'); + }); + + // ── Daten migrieren ───────────────────────────────────────────────── + + // 1. Aus lead_files → inquiry_id gesetzt; communication_id wird unten gesetzt + DB::statement(" + INSERT INTO attachments + (inquiry_id, booking_id, communication_id, + identifier, filename, dir, original_name, ext, mime_type, size, + legacy_source, legacy_id, created_at, updated_at) + SELECT + lead_id, NULL, NULL, + identifier, filename, dir, original_name, ext, mine, size, + 'lead_file', id, created_at, updated_at + FROM lead_files + "); + + // 2. Aus booking_files → booking_id gesetzt + DB::statement(" + INSERT INTO attachments + (inquiry_id, booking_id, communication_id, + identifier, filename, dir, original_name, ext, mime_type, size, + legacy_source, legacy_id, created_at, updated_at) + SELECT + NULL, booking_id, NULL, + identifier, filename, dir, original_name, ext, mine, size, + 'booking_file', id, created_at, updated_at + FROM booking_files + "); + + // 3. communication_id für lead_files setzen (sofern lead_mail_id gesetzt war) + // Nutzt legacy_source + legacy_id der communications-Tabelle. + DB::statement(" + UPDATE attachments a + INNER JOIN lead_files lf ON lf.id = a.legacy_id + INNER JOIN communications c + ON c.legacy_source = 'lead_mail' + AND c.legacy_id = lf.lead_mail_id + SET a.communication_id = c.id + WHERE a.legacy_source = 'lead_file' + AND lf.lead_mail_id IS NOT NULL + "); + } + + public function down(): void + { + Schema::dropIfExists('attachments'); + } +}; diff --git a/database/migrations/2025_04_15_400004_phase4_drop_old_communication_tables.php b/database/migrations/2025_04_15_400004_phase4_drop_old_communication_tables.php new file mode 100644 index 0000000..bbdff3f --- /dev/null +++ b/database/migrations/2025_04_15_400004_phase4_drop_old_communication_tables.php @@ -0,0 +1,45 @@ +dropForeign('lead_files_lead_mail_id_foreign'); + }); + } + + Schema::dropIfExists('lead_mails'); + Schema::dropIfExists('customer_mails'); + } + + public function down(): void + { + throw new \RuntimeException( + 'Phase 4 Schritt 2a kann nicht automatisch zurückgerollt werden. ' . + 'Bitte Datenbank-Backup einspielen.' + ); + } +}; diff --git a/database/migrations/2025_04_15_400005_phase4_drop_old_notice_tables.php b/database/migrations/2025_04_15_400005_phase4_drop_old_notice_tables.php new file mode 100644 index 0000000..d50006d --- /dev/null +++ b/database/migrations/2025_04_15_400005_phase4_drop_old_notice_tables.php @@ -0,0 +1,35 @@ +id(); + $t->string('offer_number', 32)->unique(); + + $t->foreignId('contact_id') + ->constrained('contacts') + ->restrictOnDelete(); + + $t->foreignId('inquiry_id') + ->nullable() + ->constrained('inquiries') + ->nullOnDelete(); + + $t->foreignId('booking_id') + ->nullable() + ->constrained('booking') + ->nullOnDelete(); + + $t->enum('status', [ + 'draft', + 'sent', + 'accepted', + 'declined', + 'expired', + 'withdrawn', + ])->default('draft'); + + // FK wird in 2026_04_17_100007 nachträglich gesetzt + $t->unsignedBigInteger('current_version_id')->nullable(); + + $t->foreignId('created_by')->constrained('users'); + + $t->timestamps(); + $t->softDeletes(); + + $t->index(['status', 'contact_id']); + $t->index('inquiry_id'); + }); + } + + public function down(): void + { + Schema::dropIfExists('offers'); + } +}; diff --git a/database/migrations/2026_04_17_100002_create_offer_versions_table.php b/database/migrations/2026_04_17_100002_create_offer_versions_table.php new file mode 100644 index 0000000..2cc6e30 --- /dev/null +++ b/database/migrations/2026_04_17_100002_create_offer_versions_table.php @@ -0,0 +1,81 @@ +id(); + + $t->foreignId('offer_id') + ->constrained('offers') + ->cascadeOnDelete(); + + $t->unsignedInteger('version_no'); + + $t->enum('status', [ + 'draft', + 'sent', + 'accepted', + 'declined', + 'expired', + 'superseded', + ])->default('draft'); + + $t->date('valid_until')->nullable(); + $t->decimal('total_price', 10, 2)->default(0); + + $t->string('headline')->nullable(); + $t->text('intro_text')->nullable(); + $t->longText('itinerary_text')->nullable(); + $t->text('closing_text')->nullable(); + + // offer_templates wird in Migration 4 erzeugt — FK dort, + // hier zunächst nullable + FK wird über Migration 4 nachgeholt + $t->unsignedBigInteger('template_id')->nullable(); + + $t->string('pdf_path')->nullable(); + $t->boolean('pdf_archived')->default(false); + + $t->dateTime('sent_at')->nullable(); + $t->dateTime('accepted_at')->nullable(); + + $t->enum('accepted_via', [ + 'customer_link', + 'admin', + 'email', + ])->nullable(); + + // Referenz auf zentral hinterlegte Dokument-Vorlagen, + // die mit dieser Version (als Anhang) verknüpft sind + $t->json('template_document_ids')->nullable(); + + $t->foreignId('created_by')->constrained('users'); + + $t->timestamps(); + + $t->unique(['offer_id', 'version_no']); + $t->index(['offer_id', 'status']); + }); + } + + public function down(): void + { + Schema::dropIfExists('offer_versions'); + } +}; diff --git a/database/migrations/2026_04_17_100003_create_offer_items_table.php b/database/migrations/2026_04_17_100003_create_offer_items_table.php new file mode 100644 index 0000000..c540a9b --- /dev/null +++ b/database/migrations/2026_04_17_100003_create_offer_items_table.php @@ -0,0 +1,68 @@ +id(); + + $t->foreignId('offer_version_id') + ->constrained('offer_versions') + ->cascadeOnDelete(); + + $t->unsignedInteger('position')->default(0); + + $t->enum('type', [ + 'travel', + 'service', + 'option', + 'discount', + 'insurance', + 'custom', + ]); + + $t->string('title'); + $t->text('description')->nullable(); + + $t->unsignedInteger('quantity')->default(1); + $t->decimal('price_per_unit', 10, 2)->default(0); + $t->decimal('total_price', 10, 2)->default(0); + + // Bewusst OHNE FK-Constraint — siehe Risiko R4 + $t->unsignedBigInteger('travel_program_id')->nullable(); + $t->unsignedBigInteger('fewo_lodging_id')->nullable(); + + $t->json('metadata')->nullable(); + + $t->timestamps(); + + $t->index(['offer_version_id', 'position']); + }); + } + + public function down(): void + { + Schema::dropIfExists('offer_items'); + } +}; diff --git a/database/migrations/2026_04_17_100004_create_offer_templates_table.php b/database/migrations/2026_04_17_100004_create_offer_templates_table.php new file mode 100644 index 0000000..67d4bab --- /dev/null +++ b/database/migrations/2026_04_17_100004_create_offer_templates_table.php @@ -0,0 +1,70 @@ +id(); + + // `branch` existiert schon im CRM — Vorlagen können so pro + // Filiale gepflegt werden. Eine spätere Erweiterung auf + // `organization_id` (Modul 5) erfolgt additiv. + $t->foreignId('branch_id') + ->nullable() + ->constrained('branch') + ->nullOnDelete(); + + $t->string('name'); + $t->text('description')->nullable(); + + $t->string('default_headline')->nullable(); + $t->text('default_intro')->nullable(); + $t->longText('default_itinerary')->nullable(); + $t->text('default_closing')->nullable(); + + // Array aus [{title, description, type, price_per_unit, quantity}, …] + $t->json('default_items')->nullable(); + + $t->boolean('is_active')->default(true); + $t->foreignId('created_by')->constrained('users'); + + $t->timestamps(); + $t->softDeletes(); + + $t->index(['branch_id', 'is_active']); + }); + + Schema::table('offer_versions', function (Blueprint $t) { + $t->foreign('template_id') + ->references('id') + ->on('offer_templates') + ->nullOnDelete(); + }); + } + + public function down(): void + { + Schema::table('offer_versions', function (Blueprint $t) { + $t->dropForeign(['template_id']); + }); + + Schema::dropIfExists('offer_templates'); + } +}; diff --git a/database/migrations/2026_04_17_100005_create_offer_files_table.php b/database/migrations/2026_04_17_100005_create_offer_files_table.php new file mode 100644 index 0000000..2e8442f --- /dev/null +++ b/database/migrations/2026_04_17_100005_create_offer_files_table.php @@ -0,0 +1,48 @@ +id(); + + $t->foreignId('offer_version_id') + ->constrained('offer_versions') + ->cascadeOnDelete(); + + $t->string('identifier', 64)->nullable(); + $t->string('filename'); + $t->string('dir'); + $t->string('original_name'); + $t->string('ext', 16); + $t->string('mine', 128); + $t->unsignedBigInteger('size')->default(0); + + $t->boolean('include_in_pdf')->default(true); + + $t->timestamps(); + + $t->index(['offer_version_id', 'identifier']); + }); + } + + public function down(): void + { + Schema::dropIfExists('offer_files'); + } +}; diff --git a/database/migrations/2026_04_17_100006_create_offer_access_tokens_table.php b/database/migrations/2026_04_17_100006_create_offer_access_tokens_table.php new file mode 100644 index 0000000..cd10b9c --- /dev/null +++ b/database/migrations/2026_04_17_100006_create_offer_access_tokens_table.php @@ -0,0 +1,48 @@ +id(); + + $t->foreignId('offer_id') + ->constrained('offers') + ->cascadeOnDelete(); + + $t->foreignId('offer_version_id') + ->constrained('offer_versions') + ->cascadeOnDelete(); + + $t->string('token_hash', 64)->unique(); + $t->dateTime('expires_at')->nullable(); + $t->dateTime('first_opened_at')->nullable(); + $t->dateTime('revoked_at')->nullable(); + + $t->timestamps(); + + $t->index(['offer_id', 'revoked_at']); + }); + } + + public function down(): void + { + Schema::dropIfExists('offer_access_tokens'); + } +}; diff --git a/database/migrations/2026_04_17_100007_add_offer_refs_to_offers_and_bookings.php b/database/migrations/2026_04_17_100007_add_offer_refs_to_offers_and_bookings.php new file mode 100644 index 0000000..32681ab --- /dev/null +++ b/database/migrations/2026_04_17_100007_add_offer_refs_to_offers_and_bookings.php @@ -0,0 +1,57 @@ +foreign('current_version_id') + ->references('id') + ->on('offer_versions') + ->nullOnDelete(); + }); + + Schema::table('booking', function (Blueprint $t) { + // `inquiry_id` kommt aus Modul-3 Phase 2 (war vorher `lead_id`) + $t->unsignedBigInteger('offer_id')->nullable()->after('inquiry_id'); + + $t->foreign('offer_id') + ->references('id') + ->on('offers') + ->nullOnDelete(); + + $t->index('offer_id'); + }); + } + + public function down(): void + { + Schema::table('booking', function (Blueprint $t) { + $t->dropForeign(['offer_id']); + $t->dropIndex(['offer_id']); + $t->dropColumn('offer_id'); + }); + + Schema::table('offers', function (Blueprint $t) { + $t->dropForeign(['current_version_id']); + }); + } +}; diff --git a/dev/backups/phase2-offers-2026-04-17/restore.sh b/dev/backups/phase2-offers-2026-04-17/restore.sh index e887a91..8c3afda 100755 --- a/dev/backups/phase2-offers-2026-04-17/restore.sh +++ b/dev/backups/phase2-offers-2026-04-17/restore.sh @@ -110,7 +110,13 @@ echo " git status (sollte Phase-2 + Offers-Dateien als modifiziert zeigen):" git status --short | head -40 echo echo " Verbliebene Phase-2-Marker im Code (sollten nun wieder da sein):" -grep -rn "inquiry_id" app/ 2>/dev/null | head -5 || echo " (keine gefunden — Restore eventuell nicht vollständig)" +PHASE2_HITS=$(grep -rn "inquiry_id" app/ 2>/dev/null | head -5 || true) +if [[ -n "${PHASE2_HITS}" ]]; then + echo "${PHASE2_HITS}" + echo " → OK, Phase-2-Code ist wieder aktiv." +else + echo " (keine gefunden — Restore eventuell nicht vollständig)" +fi echo echo "==========================================" diff --git a/dev/customer-bookings/phase-2-live-deploy.md b/dev/customer-bookings/phase-2-live-deploy.md new file mode 100644 index 0000000..fe367a1 --- /dev/null +++ b/dev/customer-bookings/phase-2-live-deploy.md @@ -0,0 +1,305 @@ +# Phase-2-Live-Deploy — Anleitung + +**Ausgangssituation (nach Phase-1-Live-Deploy am 2026-04-17):** +- Live: Phase 1 ✅ (Code + DB-Migrationen 25, 26) +- Test: Phase 1 ✅ + Phase 2 ✅ (Code + DB-Migrationen 25–29, verifiziert) +- Workspace (lokal): Phase 1 + Phase 2 + Offers-Code ready + +Dieses Handbuch beschreibt, wie Phase 2 vom Test-Stand auf Live übertragen wird. Phase 2 umfasst zwei atomar zusammengehörige Teile plus eine kleine Ergänzung aus Phase 1: + +1. **DB-Schema-Änderungen** (3 Migrationen, breaking): + - `RENAME TABLE customer → contacts` + - `RENAME TABLE lead → inquiries` + - Spalte `booking.lead_id → booking.inquiry_id` (inkl. FK-Index-Neuaufbau) +2. **Application-Code-Änderungen** (23 modifizierte Dateien): + - Models `Customer`, `Contact`, `Lead`, `Booking` mit neuen `$table`-Werten bzw. Spaltennamen + - Repositories, Controllers, Services, Commands, Views — alle Referenzen auf `booking.lead_id` → `inquiry_id` umgestellt; Raw-SQL auf `contacts`/`inquiries` umgestellt + - Smoke-Test-Fixes (2026-04-17): `Lead::bookings()` mit explizitem FK `inquiry_id`; `orderColumn`-SQL in `ContactController`/`ReportController`/`ReportBookingController`/`ReportLeadsController` auf `contacts.*` gezogen. +3. **Phase-1-Hotfixes — nullable Parameter in den Mail-Dir-Services** (3 modifizierte Dateien): + - `app/Services/Booking.php`, `app/Services/Lead.php`, `app/Services/MailDirService.php` + - `int $mailDirId` → `?int $mailDirId` bei `getCustomerMailName()` / `getCustomerMailEmails()` / `resolveModel()` (behebt Mail-Dialog-Crash wenn `customer_mails.mail_dir_id` / `lead_mails.mail_dir_id` NULL ist). + - `string $subdir` → `?string $subdir` bei `setOutputDirs()` / `setOutputDir()` (behebt Mail-Dialog-Crash wenn `$mail_sdir_id` NULL ist, z.B. bei Entwürfen oder Top-Level-Ordnern). + - Fachlich eigentlich Phase-1-Bugs (Laravel-10/PHP-8-strict-typing vs. `null`-Werte aus nullable DB-Spalten), die aber erst durch die Test-Durchläufe am 2026-04-17 sichtbar wurden. Wir liefern die Fixes zusammen mit Phase 2 aus, weil ein zweites separates Wartungsfenster für drei Service-Dateien nicht sinnvoll wäre und die Änderungen keinerlei DB-Abhängigkeit haben (also auch außerhalb der DB-Migration risikoarm sind). + +> **Wichtig:** DB und Phase-2-Code müssen atomar gemeinsam live gehen. Zwischen den beiden gibt es kein kompatibles Fenster, deswegen ist ein **Maintenance-Window von ~10–20 min Pflicht**. Die 3 Phase-1-Hotfix-Services werden im selben rsync-Schritt mit hochgeladen. + +--- + +## Vorbereitung (am Tag vor dem Deploy) + +1. **Timing**: Wartungsfenster einplanen, idealerweise außerhalb der Geschäftszeiten. ~20 Minuten reichen erfahrungsgemäß. +2. **Deploy-Freigabe**: Phase 1 auf Live läuft stabil (mindestens 24–48 h), keine Regressions-Meldungen. +3. **Team-Kommunikation**: Mitarbeiter:innen informieren — während des Wartungsfensters keine Mails, keine Buchungen anlegen, keine PDFs drucken. + +### Vorab-Kontrolle auf Live (lesend, keine Änderungen) + +```bash +# Live-Server: aktueller Stand? +php artisan --version +# → sollte 10.50+ sein (Phase 1 hat Laravel 10 gebracht) + +php artisan migrate:status | grep phase1 +# → sollte beide phase1-Migrationen mit "Ran" zeigen + +mysql -e "SHOW TABLES LIKE 'customer';" +mysql -e "SHOW TABLES LIKE 'lead';" +# → beide müssen noch existieren (Phase 2 NICHT ausgeführt) + +mysql -e "SHOW COLUMNS FROM booking LIKE 'lead_id';" +# → lead_id muss existieren +``` + +Wenn einer dieser Checks nicht das erwartete Ergebnis liefert: **STOP**, klären bevor weitergemacht wird. + +### Smoke-Tests auf Test (am Deploy-Tag, vor dem Live-Deploy) + +Diese Liste einmal komplett auf `https://mein.sterntours.test` durchgehen. Wenn einer fehlschlägt → Fehler fixen, Live-Deploy verschieben. + +- [ ] Login funktioniert, Dashboard lädt +- [ ] **Buchungen**: `/booking` lädt, Filter- und Sortier-Funktionen arbeiten, Anfrage-Nr-Spalte zeigt korrekte Werte +- [ ] **Buchungsdetail** öffnen, alle Tabs laden (Buchung, Mails, Notizen, Dokumente) +- [ ] **PDF-Generierung** aus einer Buchung: Buchungsauftrag, Reisebestätigung, Voucher (Kunde + Agentur), Storno +- [ ] **Anfragen**: `/lead` lädt, einzelne Anfrage öffnen +- [ ] **Anfrage → Buchung erzeugen** (legt `booking.inquiry_id` korrekt an) +- [ ] **Kontakte**: `/contacts` lädt, einzelner Kontakt öffnet, `leads_count` + `bookings_count` zeigen korrekte Werte (testet die Beziehungen über die neuen Tabellennamen) +- [ ] **Kontakt-Duplikate**: `/contact/duplicates` lädt, Zähler stimmen +- [ ] **Mail-Versand** aus einer Buchung (Eintrag in `customer_mails.lead_id` muss identisch zur `booking.inquiry_id` sein) +- [ ] **Mail-Dialog öffnen** bei Buchung und Anfrage (Modal "Neue Mail schreiben" / "Mail anzeigen") — testet den nullable-`mail_dir_id`-Fix in `Services\Booking::getCustomerMailName()` / `Services\Lead::getCustomerMailName()`. +- [ ] **Admin-Reports**: `/admin/report`, `/admin/report-bookings`, `/admin/report-provider`, `/admin/report-leads` laden UND jeweils einmal nach Kundenname sortieren (testet die `orderColumn`-Fixes für `contacts.firstname/name`) + CSV-Export +- [ ] **BookingImport über API** (falls testbar — feuert gegen `/api/booking/import`) legt Datensatz mit korrekter `inquiry_id` an +- [ ] Newsletter-Sync-Command als Dry-Run: `php artisan newsletter:sync-kulturreisen --dry-run` (falls implementiert) + +--- + +## Deploy-Ablauf Live + +### Schritt 1 — Upload-Liste generieren (lokal) + +Der Phase-2-Delta gegen Phase 1: **3 neue Migrationen + 24 modifizierte Dateien (Phase 2) + 3 Phase-1-Hotfix-Services = 30 Dateien.** Exakte Liste: + +```bash +cd /workspace/mein.sterntours.de +cat > /tmp/phase2-files-to-upload.txt <<'EOF' +database/migrations/2025_04_15_200001_phase2_rename_customer_to_contacts.php +database/migrations/2025_04_15_200002_phase2_rename_lead_to_inquiries.php +database/migrations/2025_04_15_200003_phase2_rename_booking_lead_id_to_inquiry_id.php +app/Models/Booking.php +app/Models/Customer.php +app/Models/Contact.php +app/Models/Lead.php +app/Repositories/BookingPDFRepository.php +app/Repositories/LeadRepository.php +app/Repositories/CustomerMailRepository.php +app/Http/Controllers/RequestController.php +app/Http/Controllers/API/BookingController.php +app/Http/Controllers/Admin/ReportController.php +app/Http/Controllers/Admin/ReportBookingController.php +app/Http/Controllers/Admin/ReportProviderController.php +app/Http/Controllers/Admin/ReportLeadsController.php +app/Http/Controllers/LeadController.php +app/Http/Controllers/CustomerController.php +app/Http/Controllers/ContactController.php +app/Services/BookingImport.php +app/Services/Booking.php +app/Services/Lead.php +app/Services/MailDirService.php +app/Console/Commands/SyncNewsletterKulturreisen.php +app/Console/Commands/ContactsFindDuplicates.php +app/Console/Commands/ContactsMergeDuplicates.php +resources/views/customer/mail/modal-show-mail-inner.blade.php +resources/views/contact/index.blade.php +resources/views/pdf/components/booking_head.blade.php +resources/views/pdf/components/booking_header.blade.php +EOF + +wc -l /tmp/phase2-files-to-upload.txt +# → sollte 30 zeigen (3 Migrationen + 24 Phase-2-Code + 3 Phase-1-Hotfix-Services) +``` + +**Erklärung zu den zusätzlichen Dateien gegenüber einer rein strukturellen Phase-2-Liste:** +- `ReportBookingController.php` — Smoke-Test-Fix am 2026-04-17: `orderColumn('customer.firstname/name', …)` → `contacts.*`. War schon vor dem Fix nicht perfekt, fiele aber beim ersten Sortier-Klick nach Phase-2-Rename hart auf. +- `resources/views/contact/index.blade.php` — Smoke-Test-Fix am 2026-04-17: DataTables-Column-Definition `name: 'customer.id'` → `name: 'contacts.id'` (+ passender `orderColumn('contacts.id', …)` / `filterColumn('contacts.id', …)` im `ContactController`). Yajra verwendet `name:` als raw SQL-Spalte, wenn kein `orderColumn` matched — deswegen muss der Identifier zwischen Blade und Controller konsistent sein und dem neuen Tabellennamen entsprechen. +- `app/Services/Booking.php`, `Lead.php`, `MailDirService.php` — Phase-1-Hotfix: nullable-Parameter (`?int $mailDirId`, `?string $subdir`). Behebt Laravel-10/PHP-8-strict-typing-Fehler bei Mail-Dialogen mit NULL-Werten aus `customer_mails` / `lead_mails`. + +> **Ausdrücklich NICHT mit hochladen:** Phase-3-/Phase-4-Migrationen (5+6 Stück), Offers-Migrationen (7 Stück), Offer-Models (6 Stück), `config/filesystems.php` (offer-Disk). Diese kommen erst mit späteren Modulen und würden auf Live ungewollt Tabellen anlegen oder Code-Erwartungen brechen. + +### Schritt 1b — Upload-Liste gegen den Workspace verifizieren (Sanity-Check) + +Damit sich hier kein Tippfehler einschleicht, einmal prüfen, dass alle 29 Einträge tatsächlich im Workspace existieren: + +```bash +cd /workspace/mein.sterntours.de +MISSING=0 +while read f; do + if [[ ! -f "$f" ]]; then + echo "FEHLT: $f" + MISSING=$((MISSING+1)) + fi +done < /tmp/phase2-files-to-upload.txt +echo "---" +echo "Fehlend: $MISSING (sollte 0 sein)" +echo "Gesamt: $(wc -l < /tmp/phase2-files-to-upload.txt)" +``` + +### Schritt 2 — DB-Backup auf Live + +```bash +# Auf dem Live-Server, vor dem Deploy +TIMESTAMP=$(date +%Y-%m-%d_%H%M) +mysqldump --single-transaction --routines --triggers \ + --databases DEIN_DB_NAME \ + | gzip > /backup/live-db-before-phase2-${TIMESTAMP}.sql.gz + +# Größe prüfen: +ls -lh /backup/live-db-before-phase2-${TIMESTAMP}.sql.gz +``` + +Falls `mysqldump` nicht zur Hand: das Hoster-Tool nutzen (phpMyAdmin-Export, Plesk/cPanel-Backup-Feature, etc.). + +### Schritt 3 — Maintenance-Mode an + +```bash +cd /pfad/zu/mein.sterntours.de +php artisan down --render="errors::503" --secret="phase-2-deploy-$(date +%s)" +# → Secret-URL wird ausgegeben, z.B.: +# https://domain/phase-2-deploy-1746123456 +# Damit kannst du während der Wartung selbst noch als Admin auf die Seite. +``` + +### Schritt 4 — Upload per rsync + +```bash +# Vom lokalen Workspace (oder Test-Server) auf Live +rsync -av --files-from=/tmp/phase2-files-to-upload.txt \ + /workspace/mein.sterntours.de/ \ + user@live-server:/pfad/zu/mein.sterntours.de/ +``` + +Alternative mit scp, falls rsync nicht verfügbar: +```bash +while read f; do + scp "/workspace/mein.sterntours.de/$f" "user@live-server:/pfad/zu/mein.sterntours.de/$f" +done < /tmp/phase2-files-to-upload.txt +``` + +### Schritt 5 — Code-Caches leeren + +Auf dem Live-Server: + +```bash +cd /pfad/zu/mein.sterntours.de +php artisan cache:clear +php artisan config:clear +php artisan route:clear +php artisan view:clear +composer dump-autoload --optimize +``` + +### Schritt 6 — DB-Migrationen gezielt ausführen + +> **Wichtig: KEIN `php artisan migrate` ohne `--path`!** +> Wenn man nacktes `migrate` aufruft, werden auch die im Workspace liegenden Phase-3-, Phase-4- und Offer-Migrationen ausgeführt, die noch nicht deployreif sind. Deswegen gezielt per `--path`: + +```bash +php artisan migrate --path=database/migrations/2025_04_15_200001_phase2_rename_customer_to_contacts.php --force +php artisan migrate --path=database/migrations/2025_04_15_200002_phase2_rename_lead_to_inquiries.php --force +php artisan migrate --path=database/migrations/2025_04_15_200003_phase2_rename_booking_lead_id_to_inquiry_id.php --force + +# Verifikation +php artisan migrate:status | grep phase2 +# → alle drei sollten jetzt "Ran" zeigen +``` + +Erwartete Dauer: wenige Sekunden. `RENAME TABLE` ist in MySQL eine Metadaten-Operation und läuft unabhängig von der Tabellengröße fast instantan. Der Spaltenrename `booking.lead_id → inquiry_id` ist je nach MySQL-Version ebenfalls schnell (MySQL 8+ mit `ALGORITHM=INSTANT`) oder dauert ein paar Sekunden (mit Fallback). + +### Schritt 7 — Production-Caches neu aufbauen + +```bash +php artisan config:cache +php artisan route:cache +php artisan view:cache +``` + +### Schritt 8 — Maintenance-Mode aus + +```bash +php artisan up +``` + +### Schritt 9 — Live-Smoke-Tests + +Dieselbe Checkliste wie bei Test (oben). Besonders aufpassen: +- PDF-Erzeugung (Voucher einer aktuellen Buchung) +- Mail-Versand aus einer Buchung +- Anfrage-Erstellung + daraus Buchung ableiten +- `/contacts/duplicates` öffnen — zeigt korrekte Counts + +Bei Problemen sofort zu **Rollback-Plan** (unten) wechseln. + +--- + +## Rollback-Plan (falls Phase 2 auf Live scheitert) + +Der Rollback muss ebenfalls atomar passieren: **Code und DB zusammen zurück.** + +### Schritt A — Maintenance-Mode an + +```bash +php artisan down --render="errors::503" +``` + +### Schritt B — DB zurückrollen + +Option 1 (sauber, per Migration): +```bash +php artisan migrate:rollback --path=database/migrations/2025_04_15_200003_phase2_rename_booking_lead_id_to_inquiry_id.php --force +php artisan migrate:rollback --path=database/migrations/2025_04_15_200002_phase2_rename_lead_to_inquiries.php --force +php artisan migrate:rollback --path=database/migrations/2025_04_15_200001_phase2_rename_customer_to_contacts.php --force +``` +(Die Migrationen haben symmetrische `down()`-Methoden, die die Renames rückgängig machen.) + +Option 2 (Notfall, falls `migrate:rollback` fehlschlägt): +```bash +gunzip < /backup/live-db-before-phase2-${TIMESTAMP}.sql.gz | mysql DEIN_DB_NAME +``` + +### Schritt C — Code zurück + +Entweder die pre-Phase-2-Versionen der 22 Dateien zurückspielen (z.B. aus dem DB-Backup-Zeitpunkt-Dateisystem-Backup), oder mit git Revert-Commit: +```bash +# Wenn Live-Server ein Git-Checkout ist: +git checkout -- app/ resources/ config/ +``` + +### Schritt D — Caches + Up + +```bash +php artisan cache:clear && php artisan config:clear && php artisan view:clear && php artisan route:clear +composer dump-autoload +php artisan config:cache && php artisan route:cache && php artisan view:cache +php artisan up +``` + +--- + +## Dokumentation nach erfolgreichem Live-Deploy + +1. `dev/customer-bookings/umsetzung.md` updaten: Phase 2 auf Live per 2026-MM-DD. +2. Dieses Handbuch als "durchgeführt" markieren, ggf. Learnings ergänzen. +3. Git-Commit auf Live-Server (falls Git-basiert): mit Tag `deploy/phase-2-live-2026-MM-DD`. +4. Nächster Schritt: Entscheidung, wann Phase 3 (Participants-Unification), Phase 4 (Communications-Unification) oder das Offers-Modul an die Reihe kommen. + +--- + +## Hintergrund: warum das Maintenance-Window nötig ist + +Zwischen DB-Rename und Code-Deploy ist der Zustand inkompatibel: + +| Zustand | DB hat | Code erwartet | Resultat | +|---|---|---|---| +| Vor Deploy | `customer`, `lead`, `booking.lead_id` | `customer`, `lead`, `booking.lead_id` | OK | +| DB-migriert, Code alt | `contacts`, `inquiries`, `booking.inquiry_id` | `customer`, `lead`, `booking.lead_id` | **Fehler: Table doesn't exist** | +| DB alt, Code neu | `customer`, `lead`, `booking.lead_id` | `contacts`, `inquiries`, `booking.inquiry_id` | **Fehler: Table doesn't exist** | +| Nach Deploy | `contacts`, `inquiries`, `booking.inquiry_id` | `contacts`, `inquiries`, `booking.inquiry_id` | OK | + +Deswegen: Maintenance-Mode an → Code + DB gleichzeitig → Maintenance-Mode aus. Alles innerhalb eines Fensters, in dem keine Requests an die App laufen. diff --git a/dev/customer-bookings/umsetzung.md b/dev/customer-bookings/umsetzung.md index eccfa6d..0b88e59 100644 --- a/dev/customer-bookings/umsetzung.md +++ b/dev/customer-bookings/umsetzung.md @@ -1,8 +1,10 @@ # Umsetzung: Neustrukturierung Customer / Lead / Booking -**Status:** Phase 1 auf Testsystem abgeschlossen — Contacts-Modul live; **Phase-2-App-Code vorbereitet (deploy-bereit)** +**Status:** Phase 1 auf **Live** abgeschlossen (2026-04-17); Phase 2 auf **Test** erfolgreich migriert und verifiziert — bereit für Live-Deploy. **Erstellt:** April 2025 -**Konzept:** [konzept.md](konzept.md) +**Letzte Aktualisierung:** 2026-04-17 +**Konzept:** [konzept.md](konzept.md) +**Deploy-Handbücher:** [phase-1-live-deploy.md](phase-1-live-deploy.md) · [phase-2-live-deploy.md](phase-2-live-deploy.md) --- @@ -10,12 +12,70 @@ | Phase | Status | Deployed auf Test? | Deployed auf Live? | |-------|--------|-------------------|-------------------| -| Phase 1 — Contact-Deduplizierung | ✅ Abgeschlossen | ✅ Ja | ⬜ Nein | -| Phase 1 — Contacts-Modul (neuer Code) | ✅ Abgeschlossen | ✅ Ja | ⬜ Nein | -| Phase 2 — App-Code (Models/Repos/Controller/Views auf `contacts`/`inquiries`/`inquiry_id`) | ✅ Abgeschlossen | ⬜ Nein | ⬜ Nein | -| Phase 2 — Tabellen-Migrationen (3 Migrationsdateien) | ⬜ Ausstehend (Code ist deploy-ready) | ⬜ Nein | ⬜ Nein | -| Phase 3 — Participants konsolidieren | ⬜ Ausstehend | ⬜ Nein | ⬜ Nein | -| Phase 4 — Communications / Notices / Attachments | ⬜ Ausstehend | ⬜ Nein | ⬜ Nein | +| Phase 1 — Contact-Deduplizierung | ✅ Abgeschlossen | ✅ Ja | ✅ **2026-04-17** | +| Phase 1 — Contacts-Modul (neuer Code) | ✅ Abgeschlossen | ✅ Ja | ✅ **2026-04-17** | +| Phase 2 — App-Code (Models/Repos/Controller/Views auf `contacts`/`inquiries`/`inquiry_id`) | ✅ Abgeschlossen | ✅ Ja | ⬜ Nein (Deploy-Handbuch fertig) | +| Phase 2 — Tabellen-Migrationen (3 Migrationsdateien) | ✅ Abgeschlossen | ✅ **Ja, Batch 27–29** | ⬜ Nein (Deploy-Handbuch fertig) | +| Phase 2 — Smoke-Test-Fixes 2026-04-17 (`Lead::bookings()` FK; `orderColumn`-SQL in 4 Controllern; Blade-Column-`name:` in `contact/index.blade.php`) | ✅ Abgeschlossen | ✅ Ja | ⬜ Nein (in Phase-2-Deploy gebündelt) | +| Phase-1-Hotfix — nullable Parameter (`?int $mailDirId`, `?string $subdir`) in 3 Service-Klassen (Mail-Dialoge mit NULL-Werten) | ✅ Abgeschlossen | ✅ Ja | ⬜ Nein (in Phase-2-Deploy gebündelt, siehe Handbuch) | +| Phase 3 — Participants konsolidieren | ⬜ Ausstehend (Migrationen geschrieben, Code noch nicht) | ⬜ Nein | ⬜ Nein | +| Phase 4 — Communications / Notices / Attachments | ⬜ Ausstehend (Migrationen geschrieben, Code noch nicht) | ⬜ Nein | ⬜ Nein | + +### Verifikation Phase 2 auf Test (2026-04-17) + +Smoke-Queries nach erfolgter Migration: + +``` +Tabellen: + customer NOT FOUND ← umbenannt ✓ + contacts EXISTS ← neu ✓ + lead NOT FOUND ← umbenannt ✓ + inquiries EXISTS ← neu ✓ + booking EXISTS ✓ + +booking-Spalten: + lead_id NOT FOUND ← umbenannt ✓ + inquiry_id EXISTS ← neu ✓ + +Eloquent-Queries: + Contact::count() = 19.156 (ohne merged) + Customer::count() = 22.283 (alle, inkl. merged — Legacy-Model) + Lead::count() = 19.543 + Booking::count() = 10.648 + Booking->inquiry_id funktioniert + Booking->lead() Relation mit expl. FK inquiry_id funktioniert +``` + +### Smoke-Test-Findings 2026-04-17 (Test-Durchlauf nach Phase-2-Restore) + +Während der UI-Smoke-Tests auf `mein.sterntours.test` traten drei unterschiedliche Bug-Klassen auf, alle auf Test gefixt und in die Phase-2-Deploy-Liste aufgenommen. + +**1. Eloquent-Relation ohne expliziten FK — `Lead::bookings()`** +- Fehler: `/leads` → `Column not found: 'booking.lead_id'`. +- Ursache: `$this->hasMany(Booking::class)` ohne 2. Argument ⇒ Laravel leitet den FK aus dem Model-Namen ab (`Lead` → `lead_id`), der nach dem Rename nicht mehr existiert. +- Fix: `app/Models/Lead.php` Zeile 249–253: `$this->hasMany(Booking::class, 'inquiry_id')`. + +**2. Hartcodierte Tabellenqualifier in `orderColumn`-SQL-Strings + Blade-Column-`name:`** +- Fehler A: `/contacts` → `Column not found: 'customer.id' in 'order clause'`. Behoben durch SQL-Umstellung im Controller — blieb aber bestehen, weil Yajra bei fehlendem `orderColumn`-Match den `name:`-String aus der Blade-Column-Definition als raw SQL verwendet. +- Fehler B (gleiche Ursache, 2. Runde): `/contacts` → derselbe Fehler trotz Controller-Fix. Root Cause: In `resources/views/contact/index.blade.php` Zeile 188 stand `name: 'customer.id'`. Yajra matched `orderColumn()` auf diesen `name:`-String — mein erster Fix mit `orderColumn('id', …)` hat also nicht getroffen. +- Ursache zusammengefasst: Yajra-DataTables-`orderColumn()` bekommt als 2. Argument eine Raw-SQL-Expression; **und** der `name:`-Wert einer DataTable-Column muss exakt zum 1. Argument von `orderColumn()` / `filterColumn()` passen — sonst fällt Yajra zurück auf "Name wörtlich als SQL einsetzen". +- Fix in 5 Dateien: + - `app/Http/Controllers/ContactController.php` — `orderColumn('contacts.id' / 'deleted_at', …)`, `filterColumn('contacts.id', …)` + - `resources/views/contact/index.blade.php` — Column-Name `customer.id` → `contacts.id` + - `app/Http/Controllers/Admin/ReportController.php` — 2 Blöcke (`customer.firstname $1` → `contacts.firstname $1` etc.) + - `app/Http/Controllers/Admin/ReportBookingController.php` — 2 Blöcke + - `app/Http/Controllers/Admin/ReportLeadsController.php` — `$orderByNum`-Array +- Bewusst nicht angefasst: die 1. Argumente von `addColumn` / `rawColumns` (`customer.fullName`, `lead.status_id`) — das sind reine DataTables-Frontend-Identifier, die Yajra über den Eloquent-Relation-Mechanismus auflöst (Model `Customer`/`Lead` → `$table = 'contacts'`/`'inquiries'`), und die 3-teiligen `booking.customer.firstname`-Strings in `ReportController.php` und `ReportProviderController.php` (Yajra-Relation-Notation bzw. seit jeher kein gültiges SQL, durch Phase 2 nicht schlimmer). + +**3. Strict-Type-Regression aus Phase 1 (Laravel-10/PHP-8) — zwei Varianten** +- Fehler A: `App\Services\Booking::getCustomerMailName(): Argument #2 ($mailDirId) must be of type int, null given` in `modal-show-mail-inner.blade.php` Zeile 92. +- Fehler B: `App\Services\Booking::setOutputDirs(): Argument #2 ($subdir) must be of type string, null given` in derselben kompilierten View, Zeile 101. +- Ursache: Blade-Views übergeben `$mail_dir_id` bzw. `$mail_sdir_id` direkt aus DB-Feldern, die nullable sein können (Entwurfs-Mails, Top-Level-Ordner, Mails ohne Subdir). Unter Laravel 8 / PHP 7 wurde `null` stillschweigend zu `0` / `""` gecastet; PHP 8 strict types verweigern das. +- Fix in drei Dateien: + - `app/Services/Booking.php` — `getCustomerMailName()`, `getCustomerMailEmails()`: `int $mailDirId` → `?int`; `setOutputDirs()`: `string $subdir` → `?string`. + - `app/Services/Lead.php` — identische Änderungen (symmetrische API). + - `app/Services/MailDirService.php` — `getCustomerMailName()`, `getCustomerMailEmails()`, `resolveModel()` auf `?int`; `setOutputDir()` auf `?string`; in `resolveModel()` zusätzlich ein früher `return null`-Guard. +- Das ist **fachlich ein Phase-1-Bug**, der nichts mit Customer→Contacts zu tun hat. Wir deployen ihn trotzdem zusammen mit Phase 2, weil die Services keine DB-Abhängigkeit haben und ein separates Wartungsfenster unnötig wäre. ### Was in Phase 1 umgesetzt wurde @@ -178,33 +238,46 @@ php artisan migrate:rollback --path=database/migrations/2025_04_15_100001_phase1 - Routen-Namen (`lead_detail`, `lead_index`) — bleiben als Aliase, um Links in Views/Mails/Logs nicht zu brechen. ### Schritt 1: Migrationen einspielen + +**Auf Test (2026-04-17 erfolgreich durchgeführt, Batch 27–29):** ```bash -# Backup erstellen! -php artisan migrate --path=database/migrations/2025_04_15_200001_phase2_rename_customer_to_contacts.php -php artisan migrate --path=database/migrations/2025_04_15_200002_phase2_rename_lead_to_inquiries.php -php artisan migrate --path=database/migrations/2025_04_15_200003_phase2_rename_booking_lead_id_to_inquiry_id.php +php artisan migrate --path=database/migrations/2025_04_15_200001_phase2_rename_customer_to_contacts.php --force +php artisan migrate --path=database/migrations/2025_04_15_200002_phase2_rename_lead_to_inquiries.php --force +php artisan migrate --path=database/migrations/2025_04_15_200003_phase2_rename_booking_lead_id_to_inquiry_id.php --force ``` +> **Wichtig — `--path` verwenden!** Ohne `--path` würde `php artisan migrate` auch die noch im Workspace liegenden Phase-3-, Phase-4- und Offers-Migrationen ausführen, die noch nicht deployreif sind. Einzeln per `--path` hält den Deploy auf exakt diese drei Dateien beschränkt. + +**Auf Live:** Ablauf siehe [phase-2-live-deploy.md](phase-2-live-deploy.md). Der Live-Deploy muss innerhalb eines Wartungsfensters passieren, weil DB und Code atomar zusammengehen müssen. + ### Ergebnis-Prüfung ```sql --- Tabellen vorhanden? SHOW TABLES LIKE 'contacts'; SHOW TABLES LIKE 'inquiries'; --- Spalte umbenannt? SHOW COLUMNS FROM booking LIKE 'inquiry_id'; --- FK vorhanden? SELECT * FROM information_schema.KEY_COLUMN_USAGE WHERE TABLE_NAME = 'booking' AND COLUMN_NAME = 'inquiry_id'; ``` +Oder via Laravel-Tinker: +```bash +php artisan tinker --execute=" +echo Schema::hasTable('contacts') ? 'contacts OK' : 'contacts FEHLT'; echo PHP_EOL; +echo Schema::hasTable('inquiries') ? 'inquiries OK' : 'inquiries FEHLT'; echo PHP_EOL; +echo Schema::hasColumn('booking', 'inquiry_id') ? 'booking.inquiry_id OK' : 'FEHLT'; echo PHP_EOL; +" +``` + ### Rollback Phase 2 ```bash # In umgekehrter Reihenfolge -php artisan migrate:rollback --path=database/migrations/2025_04_15_200003_phase2_rename_booking_lead_id_to_inquiry_id.php -php artisan migrate:rollback --path=database/migrations/2025_04_15_200002_phase2_rename_lead_to_inquiries.php -php artisan migrate:rollback --path=database/migrations/2025_04_15_200001_phase2_rename_customer_to_contacts.php +php artisan migrate:rollback --path=database/migrations/2025_04_15_200003_phase2_rename_booking_lead_id_to_inquiry_id.php --force +php artisan migrate:rollback --path=database/migrations/2025_04_15_200002_phase2_rename_lead_to_inquiries.php --force +php artisan migrate:rollback --path=database/migrations/2025_04_15_200001_phase2_rename_customer_to_contacts.php --force ``` +> **Wichtig:** Rollback MUSS zusammen mit Code-Revert passieren (Code erwartet sonst die falschen Tabellennamen). Details im [phase-2-live-deploy.md](phase-2-live-deploy.md#rollback-plan-falls-phase-2-auf-live-scheitert). + --- ## Phase 3 — Participants konsolidieren diff --git a/resources/views/booking/_detail_price.blade.php b/resources/views/booking/_detail_price.blade.php index 869e6e2..cd48819 100755 --- a/resources/views/booking/_detail_price.blade.php +++ b/resources/views/booking/_detail_price.blade.php @@ -121,6 +121,25 @@ + @php + $priceTotalStored = $booking->getPriceTotalRaw(); + $priceTotalComputed = round((float) $booking->getPriceRaw() + (float) $booking->getServiceTotal(true), 2); + @endphp +
+

+ price_total (gespeichert in DB): + {{ \App\Services\Util::_number_format($priceTotalStored ?? 0) }} € + · + rechnerisch (Organisation + Vermittlung): + {{ \App\Services\Util::_number_format($priceTotalComputed) }} € + @if($booking->isCanceled() && $booking->getPriceCanceledRaw() !== null) + · + laut Storno-Logik (= Storno-Betrag price_canceled): + {{ \App\Services\Util::_number_format((float) $booking->getPriceCanceledRaw()) }} € + @endif +

+
+

diff --git a/resources/views/contact/index.blade.php b/resources/views/contact/index.blade.php index b327d3d..e02edb4 100644 --- a/resources/views/contact/index.blade.php +++ b/resources/views/contact/index.blade.php @@ -185,7 +185,7 @@ }, { data: 'id', - name: 'customer.id' + name: 'contacts.id' }, { data: 'firstname', diff --git a/resources/views/customer/mail/modal-show-mail-inner.blade.php b/resources/views/customer/mail/modal-show-mail-inner.blade.php index 0c4eaf2..bff0511 100644 --- a/resources/views/customer/mail/modal-show-mail-inner.blade.php +++ b/resources/views/customer/mail/modal-show-mail-inner.blade.php @@ -45,7 +45,7 @@

Kunde: {{ $customer_mail->customer->salutation->name }} {{ $customer_mail->customer->title }} {{ $customer_mail->customer->firstname }} {{ $customer_mail->customer->name }} @if($customer_mail->booking) - ({{$customer_mail->booking->lead_id}}) + ({{$customer_mail->booking->inquiry_id}}) @endif

@endif diff --git a/resources/views/pdf/components/booking_head.blade.php b/resources/views/pdf/components/booking_head.blade.php index 50ea257..d043bb9 100644 --- a/resources/views/pdf/components/booking_head.blade.php +++ b/resources/views/pdf/components/booking_head.blade.php @@ -22,7 +22,7 @@ {{-- @endif --}} - Buchungsnummer: {{ $booking->lead_id }}
+ Buchungsnummer: {{ $booking->inquiry_id }}
Buchungsdatum: {{ _format_date($booking->booking_date) }}

Reisetermin: {{ _format_date($booking->start_date) }} - {{ _format_date($booking->end_date) }}
diff --git a/resources/views/pdf/components/booking_header.blade.php b/resources/views/pdf/components/booking_header.blade.php index 89a44c6..265985e 100644 --- a/resources/views/pdf/components/booking_header.blade.php +++ b/resources/views/pdf/components/booking_header.blade.php @@ -35,7 +35,7 @@ Buchungsnummer: - {{ $booking->lead_id }} + {{ $booking->inquiry_id }} Buchungsdatum: