Files

288 lines
10 KiB
PHP

<?php
namespace App\Http\Controllers;
use App\Contracts\PhoneNumberNormalizerInterface;
use App\Models\Contact;
use App\Models\ContactGroup;
use App\Models\SourceSyncRun;
use Illuminate\Contracts\View\View;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\DB;
use Illuminate\Validation\Rule;
class ContactController extends Controller
{
public function index(Request $request): View
{
$search = (string) $request->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<string, array<int, string>>
*/
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<string, mixed> $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);
}
}