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