pruneInvalidQueueEntries($business_id); $phones = CrmPhoneFollowUp::where('business_id', $business_id) ->where('status', 'pending') ->where('block_auto_recall', false) ->get(); foreach ($phones as $phone) { $exists = CrmCallQueue::where('business_id', $business_id) ->where('phone_follow_up_id', $phone->id) ->exists(); if (! $exists) { CrmCallQueue::create([ 'business_id' => $business_id, 'phone_follow_up_id' => $phone->id, 'campaign_id' => $phone->campaign_id, 'status' => 'available', 'priority' => $phone->priority ?? 3, ]); } } } /** * Remove queue slots for numbers already called or no longer pending. */ public function pruneInvalidQueueEntries(int $business_id): void { $now = Carbon::now(); CrmCallQueue::where('business_id', $business_id) ->whereIn('status', ['available', 'locked']) ->whereHas('phoneFollowUp', function ($q) { $q->where('status', '!=', 'pending'); }) ->update([ 'status' => 'skipped', 'completed_at' => $now, 'locked_by' => null, 'locked_at' => null, 'expires_at' => null, ]); $donePhoneIds = CrmCallQueue::where('business_id', $business_id) ->whereIn('status', ['completed', 'skipped']) ->pluck('phone_follow_up_id') ->unique(); if ($donePhoneIds->isNotEmpty()) { CrmCallQueue::where('business_id', $business_id) ->whereIn('status', ['available', 'locked']) ->whereIn('phone_follow_up_id', $donePhoneIds) ->update([ 'status' => 'skipped', 'completed_at' => $now, 'locked_by' => null, 'locked_at' => null, 'expires_at' => null, ]); } } public function acquireNext(int $business_id, int $user_id, ?int $campaign_id = null): ?CrmCallQueue { return DB::transaction(function () use ($business_id, $user_id, $campaign_id) { $this->syncQueueForBusiness($business_id); $query = CrmCallQueue::where('crm_call_queues.business_id', $business_id) ->where('crm_call_queues.status', 'available') ->join('crm_phone_follow_ups', 'crm_call_queues.phone_follow_up_id', '=', 'crm_phone_follow_ups.id') ->where('crm_phone_follow_ups.status', 'pending') ->where('crm_phone_follow_ups.block_auto_recall', false) ->select('crm_call_queues.*') ->orderBy('crm_call_queues.priority') ->orderBy('crm_call_queues.id'); if (! auth()->user()->can('crm.telemarketing.view_all_queue') && ! auth()->user()->can('superadmin')) { $query->where(function ($q) use ($user_id) { $q->whereNull('crm_phone_follow_ups.assigned_to') ->orWhere('crm_phone_follow_ups.assigned_to', $user_id); }); } if (! empty($campaign_id)) { $query->where('crm_call_queues.campaign_id', $campaign_id); } $queue = $query->lockForUpdate()->first(); if (empty($queue)) { return null; } $lockMinutes = TelemarketingUtil::callLockMinutes($business_id); $now = Carbon::now(); $queue->update([ 'status' => 'locked', 'locked_by' => $user_id, 'locked_at' => $now, 'expires_at' => $now->copy()->addMinutes($lockMinutes), ]); return $queue->fresh(['phoneFollowUp']); }); } public function release(int $queue_id, int $user_id, ?int $business_id = null): bool { $query = CrmCallQueue::query()->where('id', $queue_id); if ($business_id) { $query->where('business_id', $business_id); } $queue = $query->first(); if (empty($queue) || $queue->locked_by != $user_id) { return false; } if ($queue->status === 'in_call') { return false; } $queue->update([ 'status' => 'available', 'locked_by' => null, 'locked_at' => null, 'expires_at' => null, ]); return true; } public function startSession(int $queue_id, int $user_id, ?int $business_id = null): ?CrmCallSession { return DB::transaction(function () use ($queue_id, $user_id, $business_id) { $query = CrmCallQueue::query()->where('id', $queue_id); if ($business_id) { $query->where('business_id', $business_id); } $queue = $query->lockForUpdate()->first(); if (empty($queue) || $queue->locked_by != $user_id) { return null; } $session = CrmCallSession::create([ 'business_id' => $queue->business_id, 'queue_id' => $queue->id, 'phone_follow_up_id' => $queue->phone_follow_up_id, 'user_id' => $user_id, 'started_at' => Carbon::now(), ]); $queue->update([ 'status' => 'in_call', 'call_session_id' => $session->id, ]); return $session->load('phoneFollowUp'); }); } public function endSession(int $session_id, int $user_id, ?string $outcome = null, ?int $business_id = null): ?CrmCallSession { return DB::transaction(function () use ($session_id, $user_id, $outcome, $business_id) { $query = CrmCallSession::query()->where('id', $session_id); if ($business_id) { $query->where('business_id', $business_id); } $session = $query->first(); if (empty($session) || $session->user_id != $user_id || ! empty($session->ended_at)) { return null; } $session->update([ 'ended_at' => Carbon::now(), 'outcome' => $outcome, ]); if (! empty($session->queue_id)) { CrmCallQueue::where('id', $session->queue_id)->update([ 'status' => 'completed', 'completed_at' => Carbon::now(), ]); } return $session->fresh(); }); } public function getActiveSession(int $business_id, int $user_id): ?CrmCallSession { return CrmCallSession::where('business_id', $business_id) ->where('user_id', $user_id) ->whereNull('ended_at') ->with(['phoneFollowUp', 'queue']) ->latest('started_at') ->first(); } public function getActiveQueue(int $business_id, int $user_id): ?CrmCallQueue { return CrmCallQueue::where('business_id', $business_id) ->where('locked_by', $user_id) ->whereIn('status', ['locked', 'in_call']) ->with('phoneFollowUp') ->latest('locked_at') ->first(); } public function expireStaleLocks(): int { $expired = CrmCallQueue::whereIn('status', ['locked', 'in_call']) ->whereNotNull('expires_at') ->where('expires_at', '<', Carbon::now()) ->get(); $count = 0; foreach ($expired as $queue) { if ($queue->status === 'in_call') { continue; } $queue->update([ 'status' => 'available', 'locked_by' => null, 'locked_at' => null, 'expires_at' => null, ]); $count++; } return $count; } }