query('q', ''); $groupId = $request->integer('group'); $favoriteOnly = $request->boolean('favorite'); $selectedStatus = (string) $request->query('status', ''); $selectedMobileFilter = (string) $request->query('mobile', 'detected'); if (! in_array($selectedStatus, ['', 'active', 'favorite', 'opt_out'], true)) { $selectedStatus = ''; } if (! in_array($selectedMobileFilter, ['all', 'detected', 'non_mobile', 'invalid', 'missing'], true)) { $selectedMobileFilter = 'detected'; } $contacts = Contact::query() ->with(['groups', 'phoneNumbers']) ->withCount('groups') ->search($search) ->when($groupId > 0, function ($query) use ($groupId): void { $query->whereHas('groups', fn ($query) => $query->whereKey($groupId)); }) ->when($favoriteOnly || $selectedStatus === 'favorite', function ($query): void { $query->where('is_favorite', true); }) ->when($selectedStatus === 'opt_out', function ($query): void { $query->where('sms_opt_out', true); }) ->when($selectedStatus === 'active', function ($query): void { $query->where('sms_opt_out', false); }) ->when($selectedMobileFilter === 'detected', function ($query): void { $query->whereHas('phoneNumbers', function ($query): void { $query->where('is_valid', true) ->where(function ($query): void { $query ->where('is_mobile', true) ->orWhere('normalized_number', 'like', '+336%') ->orWhere('normalized_number', 'like', '+337%'); }); }); }) ->when($selectedMobileFilter === 'non_mobile', function ($query): void { $query->whereHas('phoneNumbers', function ($query): void { $query->where('is_valid', true) ->where('is_mobile', false) ->where('normalized_number', 'not like', '+336%') ->where('normalized_number', 'not like', '+337%'); }); }) ->when($selectedMobileFilter === 'invalid', function ($query): void { $query->whereHas('phoneNumbers', fn ($query) => $query->where('is_valid', false)); }) ->when($selectedMobileFilter === 'missing', function ($query): void { $query->whereDoesntHave('phoneNumbers'); }) ->orderByDesc('is_favorite') ->orderBy('last_name') ->orderBy('first_name') ->paginate(15) ->withQueryString(); $groups = ContactGroup::query() ->withCount('contacts') ->orderBy('name') ->get(); return view('contacts.index', [ 'contacts' => $contacts, 'groups' => $groups, 'search' => $search, 'selectedGroupId' => $groupId, 'favoriteOnly' => $favoriteOnly, 'selectedStatus' => $selectedStatus, 'selectedMobileFilter' => $selectedMobileFilter, 'cardDavSyncEnabled' => (bool) config('relais_sms.default_settings.carddav_sync_enabled'), 'lastCardDavSyncRun' => SourceSyncRun::query() ->where('source', 'carddav') ->latest('started_at') ->first(), ]); } public function create(): View { return view('contacts.create', [ 'groups' => ContactGroup::query()->where('source', 'local')->orderBy('name')->get(), ]); } public function store(Request $request, PhoneNumberNormalizerInterface $normalizer): RedirectResponse { $validated = $request->validate($this->rules()); $contact = DB::transaction(function () use ($request, $validated, $normalizer): Contact { $contact = Contact::query()->create([ 'source' => 'local', 'first_name' => $validated['first_name'] ?? null, 'last_name' => $validated['last_name'], 'organization' => $validated['organization'] ?? null, 'email' => $validated['email'] ?? null, 'notes' => $validated['notes'] ?? null, 'sms_opt_out' => $request->boolean('sms_opt_out'), 'is_favorite' => $request->boolean('is_favorite'), ]); $contact->groups()->sync($validated['group_ids'] ?? []); $this->syncPrimaryPhoneNumber($contact, $validated, $normalizer); return $contact; }); return redirect() ->route('contacts.show', $contact) ->with('status', 'Contact cree.'); } public function show(Contact $contact): View { $contact->load(['groups', 'phoneNumbers']); return view('contacts.show', [ 'contact' => $contact, ]); } public function favorite(Request $request, Contact $contact): RedirectResponse { $validated = $request->validate([ 'is_favorite' => ['required', 'boolean'], ]); $contact->update([ 'is_favorite' => (bool) $validated['is_favorite'], ]); return back()->with('status', $contact->is_favorite ? 'Contact ajoute aux favoris.' : 'Contact retire des favoris.'); } public function optOut(Request $request, Contact $contact): RedirectResponse { $validated = $request->validate([ 'sms_opt_out' => ['required', 'boolean'], ]); $contact->update([ 'sms_opt_out' => (bool) $validated['sms_opt_out'], ]); return back()->with('status', $contact->sms_opt_out ? 'Contact ajoute aux refus SMS.' : 'Contact retire des refus SMS.'); } public function edit(Contact $contact): View { $this->ensureLocalContact($contact); $contact->load(['groups', 'phoneNumbers']); return view('contacts.edit', [ 'contact' => $contact, 'groups' => ContactGroup::query()->where('source', 'local')->orderBy('name')->get(), 'primaryPhone' => $contact->phoneNumbers->firstWhere('is_primary', true) ?? $contact->phoneNumbers->first(), ]); } public function update(Request $request, Contact $contact, PhoneNumberNormalizerInterface $normalizer): RedirectResponse { $this->ensureLocalContact($contact); $validated = $request->validate($this->rules()); DB::transaction(function () use ($request, $contact, $validated, $normalizer): void { $contact->update([ 'first_name' => $validated['first_name'] ?? null, 'last_name' => $validated['last_name'], 'organization' => $validated['organization'] ?? null, 'email' => $validated['email'] ?? null, 'notes' => $validated['notes'] ?? null, 'sms_opt_out' => $request->boolean('sms_opt_out'), 'is_favorite' => $request->boolean('is_favorite'), ]); $contact->groups()->sync($validated['group_ids'] ?? []); $this->syncPrimaryPhoneNumber($contact, $validated, $normalizer); }); return redirect() ->route('contacts.show', $contact) ->with('status', 'Contact modifie.'); } public function destroy(Contact $contact): RedirectResponse { $this->ensureLocalContact($contact); $contact->delete(); return redirect() ->route('contacts.index') ->with('status', 'Contact supprime.'); } /** * @return array> */ private function rules(): array { return [ 'first_name' => ['nullable', 'string', 'max:255'], 'last_name' => ['required', 'string', 'max:255'], 'organization' => ['nullable', 'string', 'max:255'], 'email' => ['nullable', 'email', 'max:255'], 'notes' => ['nullable', 'string'], 'phone_label' => ['nullable', 'string', 'max:255'], 'raw_phone_number' => ['nullable', 'string', 'max:255'], 'group_ids' => ['array'], 'group_ids.*' => [ 'integer', Rule::exists('contact_groups', 'id') ->where('source', 'local') ->whereNull('deleted_at'), ], ]; } private function ensureLocalContact(Contact $contact): void { abort_if($contact->source !== 'local', 403, 'Les contacts importes ne sont pas modifiables localement.'); } /** * @param array $validated */ private function syncPrimaryPhoneNumber(Contact $contact, array $validated, PhoneNumberNormalizerInterface $normalizer): void { $rawPhoneNumber = trim((string) ($validated['raw_phone_number'] ?? '')); $primaryPhone = $contact->phoneNumbers()->where('is_primary', true)->first() ?? $contact->phoneNumbers()->first(); if ($rawPhoneNumber === '') { $primaryPhone?->delete(); return; } $result = $normalizer->normalize( $rawPhoneNumber, (string) config('relais_sms.default_settings.default_country'), ); $attributes = [ 'label' => $validated['phone_label'] ?? null, 'raw_number' => $result->rawNumber, 'normalized_number' => $result->normalizedNumber, 'country' => $result->country, 'is_valid' => $result->isValid, 'is_mobile' => $result->isMobile, 'is_primary' => true, 'validation_error' => $result->error, ]; if ($primaryPhone) { $primaryPhone->update($attributes); return; } $contact->phoneNumbers()->create($attributes); } }