diff --git a/app/Http/Controllers/API/BookingController.php b/app/Http/Controllers/API/BookingController.php index e47cd07..a020ad4 100755 --- a/app/Http/Controllers/API/BookingController.php +++ b/app/Http/Controllers/API/BookingController.php @@ -8,42 +8,35 @@ use App\Services\BookingImport; class BookingController extends Controller { - private $successStatus = 200; - private $successKey = 'f6077389c9ce710e554763a5de02c8ec'; - - protected $draftRepo; + private int $successStatus = 200; + private string $successKey; public function __construct() { - + $this->successKey = config('app.success_key'); } public function import() { $request = \Request::all(); - if(!isset($request['key']) || $request['key'] !== $this->successKey){ + if (!isset($request['key']) || $request['key'] !== $this->successKey) { return response()->json(['error' => "key"], 401); } $travel_booking = TravelBooking::find($request['travel_booking_id']); - //# vor testing - //$travel_booking = TravelBooking::find(2922); - if(!isset($travel_booking) || !$travel_booking){ + if (!$travel_booking) { return response()->json(['error' => 'no-booking-found'], $this->successStatus); } $booking = BookingImport::importFrom($travel_booking); - $ret= [ - 'url_v1' => make_old_url('/index.php/booking/'.$booking->id.'/edit'), + $ret = [ + 'url_v1' => make_old_url('/index.php/booking/' . $booking->id . '/edit'), 'url_v3' => route('booking_detail', $booking->id), 'lead_id' => $booking->lead_id ]; return response()->json(['success' => "import", "ret" => $ret], $this->successStatus); - //return response()->json(['error' => 'no-node'], $this->successStatus); } - - } diff --git a/app/Http/Controllers/ContactController.php b/app/Http/Controllers/ContactController.php new file mode 100644 index 0000000..08ef241 --- /dev/null +++ b/app/Http/Controllers/ContactController.php @@ -0,0 +1,312 @@ +middleware(['admin', '2fa']); + } + + public function index(): \Illuminate\View\View + { + return view('contact.index'); + } + + public function detail(string $id): \Illuminate\View\View + { + if ($id === 'new') { + $contact = new Contact(); + } else { + $contact = Contact::with(['salutation', 'leads', 'bookings', 'mergedContacts']) + ->findOrFail((int) $id); + } + + return view('contact.detail', [ + 'contact' => $contact, + 'id' => $id, + ]); + } + + public function store(Request $request, string $id): \Illuminate\Http\RedirectResponse + { + $data = $request->except('_token'); + + if (!isset($data['action'])) { + abort(403, 'keine Action'); + } + + if ($id === 'new') { + $contact = $this->contactRepo->createContact($data); + \Session()->flash('alert-save', '1'); + return redirect(route('contact_detail', [$contact->id]) . '#collapseContactDetail'); + } + + $this->contactRepo->updateContact($id, $data); + \Session()->flash('alert-save', '1'); + + return redirect(route('contact_detail', [$id]) . '#collapseContactDetail'); + } + + public function history(int $id): \Illuminate\View\View + { + $contact = Contact::with([ + 'leads.sf_guard_user', + 'bookings.travel_country', + 'bookings.travel_agenda', + 'bookings.sf_guard_user', + 'bookings.lead', + ])->findOrFail($id); + + return view('contact._detail_history', [ + 'contact' => $contact, + 'modal' => true, + ]); + } + + public function destroy(int $id): \Illuminate\Http\JsonResponse + { + $contact = Contact::withoutGlobalScope('not_merged')->findOrFail($id); + + $leadsCount = $contact->leads()->count(); + $bookingCount = $contact->bookings()->count(); + + if ($leadsCount > 0 || $bookingCount > 0) { + return response()->json([ + 'success' => false, + 'message' => sprintf( + 'Kontakt kann nicht gelöscht werden: %s%s vorhanden.', + $leadsCount > 0 ? $leadsCount . ' Anfrage(n)' : '', + $bookingCount > 0 ? ($leadsCount > 0 ? ' und ' : '') . $bookingCount . ' Buchung(en)' : '' + ), + ], 422); + } + + $contact->delete(); + + return response()->json(['success' => true]); + } + + public function duplicates(): \Illuminate\View\View + { + $counts = [ + 'HIGH' => $this->countDuplicateGroups('email'), + 'MEDIUM' => $this->countDuplicateGroups('name_birthdate'), + 'LOW' => $this->countDuplicateGroups('name_zip'), + ]; + + return view('contact.duplicates', compact('counts')); + } + + public function getDuplicateGroups(Request $request): \Illuminate\Http\JsonResponse + { + $confidence = strtoupper($request->input('confidence', 'HIGH')); + + $groups = match ($confidence) { + 'HIGH' => $this->findByEmail(), + 'MEDIUM' => $this->findByNameBirthdate(), + 'LOW' => $this->findByNameZip(), + default => [], + }; + + // Für jede Gruppe die vollständigen Kontakt-Daten laden + $result = []; + foreach ($groups as $ids) { + $contacts = Contact::withoutGlobalScopes() + ->withCount(['leads', 'bookings']) + ->whereIn('id', $ids) + ->whereNull('merged_into_id') + ->whereNull('deleted_at') + ->orderByRaw('FIELD(id, ' . implode(',', $ids) . ')') + ->get(['id', 'firstname', 'name', 'email', 'zip', 'city', 'phone', 'phonemobile', 'birthdate', 'created_at', 'updated_at']); + + if ($contacts->count() < 2) { + continue; + } + + $result[] = [ + 'master' => $contacts->first(), + 'duplicates' => $contacts->skip(1)->values(), + ]; + } + + return response()->json($result); + } + + public function merge(Request $request): \Illuminate\Http\JsonResponse + { + $masterId = (int) $request->input('master_id'); + $dupeId = (int) $request->input('duplicate_id'); + + if (!$masterId || !$dupeId || $masterId === $dupeId) { + return response()->json(['success' => false, 'message' => 'Ungültige IDs.'], 422); + } + + $master = Contact::withoutGlobalScopes()->find($masterId); + $dupe = Contact::withoutGlobalScopes()->find($dupeId); + + if (!$master || !$dupe) { + return response()->json(['success' => false, 'message' => 'Kontakt nicht gefunden.'], 404); + } + + DB::transaction(function () use ($masterId, $dupeId) { + DB::table('lead')->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([ + 'merged_into_id' => $masterId, + 'merged_at' => now(), + ]); + }); + + return response()->json(['success' => true]); + } + + // ── Duplikat-Hilfs-Queries ──────────────────────────────────────────────── + + private function countDuplicateGroups(string $type): int + { + return match ($type) { + 'email' => DB::table('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(), + default => 0, + }; + } + + private function findByEmail(): array + { + return DB::table('customer') + ->selectRaw('GROUP_CONCAT(id ORDER BY updated_at DESC, id DESC) as ids') + ->whereNotNull('email')->where('email', '!=', '') + ->whereNull('merged_into_id')->whereNull('deleted_at') + ->groupBy('email')->havingRaw('COUNT(*) > 1') + ->pluck('ids')->map(fn ($s) => array_map('intval', explode(',', $s)))->all(); + } + + private function findByNameBirthdate(): array + { + return DB::table('customer') + ->selectRaw('GROUP_CONCAT(id ORDER BY updated_at DESC, id DESC) as ids') + ->whereNotNull('name')->whereNotNull('firstname')->whereNotNull('birthdate') + ->whereNull('merged_into_id')->whereNull('deleted_at') + ->groupBy('name', 'firstname', 'birthdate')->havingRaw('COUNT(*) > 1') + ->pluck('ids')->map(fn ($s) => array_map('intval', explode(',', $s)))->all(); + } + + private function findByNameZip(): array + { + return DB::table('customer') + ->selectRaw('GROUP_CONCAT(id ORDER BY updated_at DESC, id DESC) as ids') + ->whereNotNull('name')->whereNotNull('firstname') + ->whereNotNull('zip')->where('zip', '!=', '') + ->whereNull('merged_into_id')->whereNull('deleted_at') + ->groupBy('name', 'firstname', 'zip')->havingRaw('COUNT(*) > 1') + ->pluck('ids')->map(fn ($s) => array_map('intval', explode(',', $s)))->all(); + } + + public function restore(int $id): \Illuminate\Http\JsonResponse + { + $contact = Contact::onlyTrashed()->withoutGlobalScope('not_merged')->findOrFail($id); + $contact->restore(); + + return response()->json(['success' => true]); + } + + public function getContacts(Request $request): \Illuminate\Http\JsonResponse + { + $showDeleted = $request->filled('filter_deleted'); + + // Reihenfolge wichtig: select() zuerst, dann withCount() — sonst überschreibt + // select() die COUNT-Subqueries die withCount() per addSelect() eingetragen hat. + $query = $showDeleted + ? Contact::onlyTrashed()->withoutGlobalScope('not_merged')->select('customer.*')->withCount(['leads', 'bookings']) + : Contact::select('customer.*')->withCount(['leads', 'bookings']); + + // Zusatzfilter aus der UI (werden als extra GET-Parameter gesendet) + if ($request->filled('filter_has_leads')) { + $query->has('leads'); + } + if ($request->filled('filter_has_bookings')) { + $query->has('bookings'); + } + if ($request->filled('filter_has_email')) { + $query->whereNotNull('email')->where('email', '!=', ''); + } + + return DataTables::eloquent($query) + ->addColumn('action_edit', function (Contact $contact) use ($showDeleted) { + if ($showDeleted) { + return ''; + } + return ''; + }) + ->addColumn('action_delete', function (Contact $contact) use ($showDeleted) { + if ($showDeleted) { + return ''; + } + return ''; + }) + ->addColumn('id', function (Contact $contact) use ($showDeleted) { + if ($showDeleted) { + return '' . $contact->id . ''; + } + return '' . $contact->id . ''; + }) + ->addColumn('raw_id', fn(Contact $contact) => $contact->id) + ->addColumn('leads_count', fn(Contact $contact) => $contact->leads_count) + ->addColumn('bookings_count', fn(Contact $contact) => $contact->bookings_count) + ->addColumn('deleted_at', fn(Contact $contact) => $contact->deleted_at?->format('d.m.Y H:i') ?? '') + ->orderColumn('id', 'customer.id $1') + ->orderColumn('deleted_at', 'customer.deleted_at $1') + ->filterColumn('id', function ($query, $keyword) { + if ($keyword !== '') { + $query->where('customer.id', 'LIKE', '%' . $keyword . '%'); + } + }) + ->filterColumn('name', function ($query, $keyword) { + if ($keyword !== '') { + $query->where(function ($q) use ($keyword) { + $q->where('name', 'LIKE', '%' . $keyword . '%') + ->orWhere('firstname', 'LIKE', '%' . $keyword . '%'); + }); + } + }) + ->filter(function ($query) use ($request) { + $location = $request->input('filter_location'); + if ($location && $location !== '') { + $query->where(function ($q) use ($location) { + $q->where('zip', 'LIKE', '%' . $location . '%') + ->orWhere('city', 'LIKE', '%' . $location . '%'); + }); + } + + $search = $request->input('search.value'); + if ($search && $search !== '') { + $query->where(function ($q) use ($search) { + $q->where('name', 'LIKE', '%' . $search . '%') + ->orWhere('firstname', 'LIKE', '%' . $search . '%') + ->orWhere('email', 'LIKE', '%' . $search . '%') + ->orWhere('phone', 'LIKE', '%' . $search . '%') + ->orWhere('phonemobile', 'LIKE', '%' . $search . '%'); + }); + } + }, true) + ->rawColumns(['action_edit', 'action_delete', 'id']) + ->make(true); + } +}