271 خطوط
11 KiB
PHP
271 خطوط
11 KiB
PHP
<?php
|
|
|
|
namespace App\Http\Controllers\Api;
|
|
|
|
use App\Http\Controllers\Controller;
|
|
use App\Models\Company;
|
|
use App\Models\Contact;
|
|
use App\Models\ContactPhone;
|
|
use App\Models\Deal;
|
|
use App\Models\Lead;
|
|
use App\Services\ActivityLogger;
|
|
use App\Support\AccessControl;
|
|
use Illuminate\Http\JsonResponse;
|
|
use Illuminate\Http\Request;
|
|
use Illuminate\Support\Facades\DB;
|
|
use Illuminate\Support\Facades\Gate;
|
|
use Illuminate\Validation\ValidationException;
|
|
|
|
class ContactController extends Controller
|
|
{
|
|
public function index(Request $request): JsonResponse
|
|
{
|
|
Gate::authorize('viewAny', Contact::class);
|
|
$query = Contact::with('phones', 'lead:id,company,first_name,last_name', 'company:id,name', 'deal:id,title');
|
|
AccessControl::scopeContacts($query, auth()->user());
|
|
|
|
if ($request->lead_id) {
|
|
$query->where('lead_id', $request->lead_id);
|
|
}
|
|
if ($request->company_id) {
|
|
$query->where('company_id', $request->company_id);
|
|
}
|
|
if ($request->deal_id) {
|
|
$query->where('deal_id', $request->deal_id);
|
|
}
|
|
if ($request->status) {
|
|
$query->where('status', $request->status);
|
|
}
|
|
if ($request->search) {
|
|
$search = $request->search;
|
|
$query->where(fn ($q) => $q->where('name', 'like', "%{$search}%")->orWhere('email', 'like', "%{$search}%"));
|
|
}
|
|
|
|
return response()->json($query->latest()->paginate(min((int) $request->get('per_page', 15), 100)));
|
|
}
|
|
|
|
public function storeStandalone(Request $request): JsonResponse
|
|
{
|
|
Gate::authorize('create', Contact::class);
|
|
$validated = $this->validateContact($request);
|
|
$this->authorizeParents($validated, 'update');
|
|
$contact = DB::transaction(fn () => $this->createContact($validated));
|
|
ActivityLogger::log('contact_created', "Contact {$contact->name} created", $contact);
|
|
|
|
return response()->json($contact->load('phones.lastCaller', 'company:id,name', 'lead:id,company'), 201);
|
|
}
|
|
|
|
public function store(Request $request, Lead $lead): JsonResponse
|
|
{
|
|
$this->authorizeLeadAccess($lead);
|
|
|
|
$validated = $this->validateContact($request);
|
|
$validated['lead_id'] = $lead->id;
|
|
$this->authorizeParents($validated, 'update');
|
|
|
|
$contact = DB::transaction(function () use ($lead, $validated) {
|
|
if (! empty($validated['is_primary'])) {
|
|
Contact::where('lead_id', $lead->id)->update(['is_primary' => false]);
|
|
}
|
|
|
|
$contact = $this->createContact($validated);
|
|
|
|
if ($contact->is_primary) {
|
|
ActivityLogger::log('primary_contact_changed', "Primary contact changed to {$contact->name}", $lead);
|
|
}
|
|
|
|
return $contact;
|
|
});
|
|
|
|
ActivityLogger::log('contact_created', "Contact {$contact->name} created for lead {$lead->id}", $contact);
|
|
|
|
return response()->json($contact->load('phones.lastCaller'), 201);
|
|
}
|
|
|
|
public function setPrimary(Request $request, Contact $contact): JsonResponse
|
|
{
|
|
Gate::authorize('update', $contact);
|
|
|
|
$validated = $request->validate([
|
|
'reason' => 'nullable|string|max:255',
|
|
]);
|
|
|
|
DB::transaction(function () use ($contact, $validated) {
|
|
$this->primaryScope($contact)->whereKeyNot($contact->id)->update(['is_primary' => false]);
|
|
$contact->update([
|
|
'is_primary' => true,
|
|
'primary_reason' => $validated['reason'] ?? 'انتخاب دستی توسط کاربر',
|
|
]);
|
|
});
|
|
|
|
ActivityLogger::log('primary_contact_changed', "Primary contact changed to {$contact->name}", $contact->lead);
|
|
|
|
return response()->json($contact->fresh('phones.lastCaller'));
|
|
}
|
|
|
|
public function update(Request $request, Contact $contact): JsonResponse
|
|
{
|
|
Gate::authorize('update', $contact);
|
|
$validated = $this->validateContact($request, true);
|
|
$this->authorizeParents($validated, 'update', $contact);
|
|
DB::transaction(function () use ($contact, $validated, $request): void {
|
|
$beforePhoneIds = $contact->phones()->withTrashed()->pluck('id')->all();
|
|
$contact->update(collect($validated)->except('phones')->all());
|
|
if ($contact->is_primary) {
|
|
$this->primaryScope($contact)->whereKeyNot($contact->id)->update(['is_primary' => false]);
|
|
}
|
|
if ($request->has('phones')) {
|
|
$keptIds = [];
|
|
foreach ($validated['phones'] ?? [] as $phone) {
|
|
if (! empty($phone['id'])) {
|
|
$existing = $contact->phones()->withTrashed()->findOrFail($phone['id']);
|
|
$existing->restore();
|
|
$existing->update([
|
|
'phone' => $phone['phone'],
|
|
'type' => $phone['type'] ?? $existing->type,
|
|
'status' => $phone['status'] ?? 'active',
|
|
]);
|
|
$keptIds[] = $existing->id;
|
|
} else {
|
|
$keptIds[] = ContactPhone::create([
|
|
'contact_id' => $contact->id,
|
|
'phone' => $phone['phone'],
|
|
'type' => $phone['type'] ?? 'mobile',
|
|
'status' => $phone['status'] ?? 'active',
|
|
])->id;
|
|
}
|
|
}
|
|
$contact->phones()->whereNotIn('id', $keptIds)->get()->each(function (ContactPhone $phone): void {
|
|
$phone->update(['status' => 'inactive']);
|
|
$phone->delete();
|
|
});
|
|
ActivityLogger::log('contact_phones_updated', "Contact {$contact->id} phones updated", $contact, ['phone_ids' => $beforePhoneIds], ['phone_ids' => $keptIds]);
|
|
}
|
|
});
|
|
ActivityLogger::log('contact_updated', "Contact {$contact->name} updated", $contact);
|
|
|
|
return response()->json($contact->fresh('phones.lastCaller'));
|
|
}
|
|
|
|
public function destroy(Contact $contact): JsonResponse
|
|
{
|
|
Gate::authorize('delete', $contact);
|
|
$contact->delete();
|
|
ActivityLogger::log('contact_deleted', "Contact {$contact->id} deleted");
|
|
|
|
return response()->json(['message' => 'مخاطب حذف شد']);
|
|
}
|
|
|
|
private function authorizeLeadAccess(Lead $lead): void
|
|
{
|
|
Gate::authorize('update', $lead);
|
|
}
|
|
|
|
private function authorizeParents(array $validated, string $ability, ?Contact $contact = null): void
|
|
{
|
|
$parentIds = [];
|
|
foreach (['lead_id' => Lead::class, 'company_id' => Company::class, 'deal_id' => Deal::class] as $key => $model) {
|
|
$parentIds[$key] = array_key_exists($key, $validated)
|
|
? $validated[$key]
|
|
: $contact?->{$key};
|
|
|
|
if (! empty($parentIds[$key])) {
|
|
Gate::authorize($ability, $model::findOrFail($parentIds[$key]));
|
|
}
|
|
}
|
|
|
|
$lead = $parentIds['lead_id'] ? Lead::findOrFail($parentIds['lead_id']) : null;
|
|
$deal = $parentIds['deal_id'] ? Deal::findOrFail($parentIds['deal_id']) : null;
|
|
$companyId = $parentIds['company_id'] ? (int) $parentIds['company_id'] : null;
|
|
|
|
$incompatible = ($lead?->company_id && $companyId && (int) $lead->company_id !== $companyId)
|
|
|| ($deal?->company_id && $companyId && (int) $deal->company_id !== $companyId)
|
|
|| ($deal?->lead_id && $lead && (int) $deal->lead_id !== (int) $lead->id);
|
|
|
|
if ($incompatible) {
|
|
throw ValidationException::withMessages([
|
|
'relationships' => 'شرکت، سرنخ و معامله انتخابشده با یکدیگر سازگار نیستند.',
|
|
]);
|
|
}
|
|
}
|
|
|
|
private function validateContact(Request $request, bool $partial = false): array
|
|
{
|
|
$sometimes = $partial ? 'sometimes|' : '';
|
|
|
|
return $request->validate([
|
|
'lead_id' => $sometimes.'nullable|exists:leads,id',
|
|
'company_id' => $sometimes.'nullable|exists:companies,id',
|
|
'deal_id' => $sometimes.'nullable|exists:deals,id',
|
|
'name' => $sometimes.'required|string|max:255',
|
|
'first_name' => 'nullable|string|max:120',
|
|
'last_name' => 'nullable|string|max:120',
|
|
'role' => 'nullable|string|max:80',
|
|
'job_title' => 'nullable|string|max:120',
|
|
'email' => 'nullable|email|max:255',
|
|
'preferred_channel' => 'nullable|string|max:40',
|
|
'description' => 'nullable|string',
|
|
'status' => 'nullable|string|max:30',
|
|
'is_primary' => 'nullable|boolean',
|
|
'primary_reason' => 'nullable|string|max:255',
|
|
'phones' => ($partial ? 'nullable' : 'required').'|array|min:1',
|
|
'phones.*.phone' => 'required|string|max:30',
|
|
'phones.*.id' => 'nullable|integer|exists:contact_phones,id',
|
|
'phones.*.type' => 'nullable|string|in:mobile,landline,extension',
|
|
'phones.*.status' => 'nullable|string|max:30',
|
|
]);
|
|
}
|
|
|
|
private function createContact(array $validated): Contact
|
|
{
|
|
$contact = Contact::create([
|
|
'lead_id' => $validated['lead_id'] ?? null,
|
|
'company_id' => $validated['company_id'] ?? null,
|
|
'deal_id' => $validated['deal_id'] ?? null,
|
|
'name' => $validated['name'],
|
|
'first_name' => $validated['first_name'] ?? null,
|
|
'last_name' => $validated['last_name'] ?? null,
|
|
'role' => $validated['role'] ?? null,
|
|
'job_title' => $validated['job_title'] ?? null,
|
|
'email' => $validated['email'] ?? null,
|
|
'preferred_channel' => $validated['preferred_channel'] ?? null,
|
|
'description' => $validated['description'] ?? null,
|
|
'status' => $validated['status'] ?? 'active',
|
|
'is_primary' => (bool) ($validated['is_primary'] ?? false),
|
|
'primary_reason' => $validated['primary_reason'] ?? (! empty($validated['is_primary']) ? 'انتخاب دستی توسط کاربر' : null),
|
|
'created_by' => auth()->id(),
|
|
]);
|
|
|
|
if ($contact->is_primary) {
|
|
$this->primaryScope($contact)->whereKeyNot($contact->id)->update(['is_primary' => false]);
|
|
}
|
|
|
|
foreach ($validated['phones'] as $phone) {
|
|
ContactPhone::create([
|
|
'contact_id' => $contact->id,
|
|
'phone' => $phone['phone'],
|
|
'type' => $phone['type'] ?? 'mobile',
|
|
'status' => $phone['status'] ?? 'active',
|
|
]);
|
|
}
|
|
|
|
return $contact;
|
|
}
|
|
|
|
private function primaryScope(Contact $contact)
|
|
{
|
|
$query = Contact::query();
|
|
if ($contact->lead_id) {
|
|
return $query->where('lead_id', $contact->lead_id);
|
|
}
|
|
if ($contact->company_id) {
|
|
return $query->whereNull('lead_id')->where('company_id', $contact->company_id);
|
|
}
|
|
if ($contact->deal_id) {
|
|
return $query->whereNull('lead_id')->whereNull('company_id')->where('deal_id', $contact->deal_id);
|
|
}
|
|
|
|
return $query->whereNull('lead_id')->whereNull('company_id')->whereNull('deal_id')->where('created_by', $contact->created_by);
|
|
}
|
|
}
|