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); } }