commonUtil = $commonUtil; $this->moduleUtil = $moduleUtil; $this->phoneListUtil = $phoneListUtil; } private function authorizeAccess(): void { $business_id = request()->session()->get('user.business_id'); $can_access_all = auth()->user()->can('crm.access_all_leads'); $can_access_own = auth()->user()->can('crm.access_own_leads'); if (! (auth()->user()->can('superadmin') || $this->moduleUtil->hasThePermissionInSubscription($business_id, 'crm_module')) || ! ($can_access_all || $can_access_own)) { abort(403, 'Unauthorized action.'); } } private function baseQuery(int $business_id) { $query = CrmPhoneFollowUp::where('crm_phone_follow_ups.business_id', $business_id); if (! auth()->user()->can('crm.access_all_leads') && auth()->user()->can('crm.access_own_leads')) { $query->OnlyOwn(); } return $query; } public function index() { $this->authorizeAccess(); $business_id = request()->session()->get('user.business_id'); if (request()->ajax()) { $query = $this->baseQuery($business_id) ->leftJoin('users as assigned_users', 'crm_phone_follow_ups.assigned_to', '=', 'assigned_users.id') ->leftJoin('users as created_users', 'crm_phone_follow_ups.created_by', '=', 'created_users.id') ->select( 'crm_phone_follow_ups.*', \DB::raw("CONCAT(COALESCE(assigned_users.surname, ''), ' ', COALESCE(assigned_users.first_name, ''), ' ', COALESCE(assigned_users.last_name, '')) as assigned_user_name"), \DB::raw("CONCAT(COALESCE(created_users.surname, ''), ' ', COALESCE(created_users.first_name, ''), ' ', COALESCE(created_users.last_name, '')) as created_user_name") ); if (! empty(request()->get('status'))) { $query->where('crm_phone_follow_ups.status', request()->get('status')); } if (! empty(request()->get('assigned_to'))) { $query->where('crm_phone_follow_ups.assigned_to', request()->get('assigned_to')); } $statuses = CrmPhoneFollowUp::statusDropdown(); return Datatables::of($query) ->addColumn('action', function ($row) { $html = '
'; return $html; }) ->editColumn('status', function ($row) use ($statuses) { $label = $statuses[$row->status] ?? $row->status; $class = match ($row->status) { 'converted' => 'label-success', 'interested' => 'label-primary', 'contacted' => 'label-info', 'no_answer' => 'label-warning', 'not_interested' => 'label-default', default => 'label-default', }; return ''.$label.''; }) ->editColumn('last_follow_up_at', '@if(!empty($last_follow_up_at)) @format_datetime($last_follow_up_at) @else - @endif') ->editColumn('lead_id', function ($row) { if (empty($row->lead_id)) { return '-'; } $url = action([LeadController::class, 'show'], ['lead' => $row->lead_id]); return ''.__('crm::lang.view_lead').''; }) ->addColumn('call_logs', function ($row) { if (! config('constants.enable_crm_call_log')) { return '-'; } $count = $this->phoneListUtil->getCallLogCount($row->business_id, $row->mobile_number); return $count > 0 ? ''.$count.' '.__('crm::lang.call_log').'' : '-'; }) ->rawColumns(['action', 'status', 'lead_id', 'call_logs']) ->filterColumn('mobile_number', function ($query, $keyword) { $query->where('crm_phone_follow_ups.mobile_number', 'like', "%{$keyword}%"); }) ->filterColumn('contact_name', function ($query, $keyword) { $query->where('crm_phone_follow_ups.contact_name', 'like', "%{$keyword}%"); }) ->filterColumn('status', function ($query, $keyword) { $query->where('crm_phone_follow_ups.status', 'like', "%{$keyword}%"); }) ->orderColumn('mobile_number', 'crm_phone_follow_ups.mobile_number $1') ->orderColumn('contact_name', 'crm_phone_follow_ups.contact_name $1') ->orderColumn('status', 'crm_phone_follow_ups.status $1') ->orderColumn('follow_up_count', 'crm_phone_follow_ups.follow_up_count $1') ->orderColumn('last_follow_up_at', 'crm_phone_follow_ups.last_follow_up_at $1') ->filterColumn('assigned_user_name', function ($query, $keyword) { $query->whereRaw("CONCAT(COALESCE(assigned_users.surname, ''), ' ', COALESCE(assigned_users.first_name, ''), ' ', COALESCE(assigned_users.last_name, '')) like ?", ["%{$keyword}%"]); }) ->filterColumn('created_user_name', function ($query, $keyword) { $query->whereRaw("CONCAT(COALESCE(created_users.surname, ''), ' ', COALESCE(created_users.first_name, ''), ' ', COALESCE(created_users.last_name, '')) like ?", ["%{$keyword}%"]); }) ->make(true); } $users = User::forDropdown($business_id, false, false, false, true); $statuses = CrmPhoneFollowUp::statusDropdown(); return view('crm::phone_list.index')->with(compact('users', 'statuses')); } public function create() { $this->authorizeAccess(); $business_id = request()->session()->get('user.business_id'); $users = User::forDropdown($business_id, false, false, false, true); $statuses = CrmPhoneFollowUp::statusDropdown(); return view('crm::phone_list.create')->with(compact('users', 'statuses')); } public function store(Request $request) { $this->authorizeAccess(); $business_id = request()->session()->get('user.business_id'); try { $request->validate([ 'mobile_numbers' => 'required_without:mobile_number|nullable|string', 'mobile_number' => 'required_without:mobile_numbers|nullable|string|max:191', 'contact_name' => 'nullable|string|max:191', 'assigned_to' => 'nullable|integer', 'notes' => 'nullable|string', ]); $numbers = []; if ($request->filled('mobile_numbers')) { $lines = preg_split('/\r\n|\r|\n/', $request->input('mobile_numbers')); foreach ($lines as $line) { $line = trim($line); if (! empty($line)) { $numbers[] = $line; } } } elseif ($request->filled('mobile_number')) { $numbers[] = trim($request->input('mobile_number')); } if (empty($numbers)) { return ['success' => false, 'msg' => __('crm::lang.phone_number_required')]; } $created = 0; $skipped = 0; $user_id = $request->session()->get('user.id'); foreach ($numbers as $number) { $exists = CrmPhoneFollowUp::where('business_id', $business_id) ->where('mobile_number', $number) ->where('status', '!=', 'converted') ->exists(); if ($exists) { $skipped++; continue; } CrmPhoneFollowUp::create([ 'business_id' => $business_id, 'mobile_number' => $number, 'contact_name' => $request->input('contact_name'), 'notes' => $request->input('notes'), 'status' => 'pending', 'assigned_to' => $request->input('assigned_to'), 'created_by' => $user_id, ]); $created++; } $msg = __('crm::lang.phone_list_added', ['count' => $created]); if ($skipped > 0) { $msg .= ' '.__('crm::lang.phone_list_skipped', ['count' => $skipped]); } return ['success' => true, 'msg' => $msg]; } catch (\Exception $e) { \Log::emergency('File:'.$e->getFile().'Line:'.$e->getLine().'Message:'.$e->getMessage()); return ['success' => false, 'msg' => __('messages.something_went_wrong')]; } } public function show($id) { $this->authorizeAccess(); $business_id = request()->session()->get('user.business_id'); $phone = $this->baseQuery($business_id) ->with(['logs.createdBy', 'assignedUser', 'lead']) ->findOrFail($id); $statuses = CrmPhoneFollowUp::statusDropdown(); $call_logs = $this->phoneListUtil->getCallLogsForPhone($phone); return view('crm::phone_list.show')->with(compact('phone', 'statuses', 'call_logs')); } public function edit($id) { $this->authorizeAccess(); $business_id = request()->session()->get('user.business_id'); $phone = $this->baseQuery($business_id)->findOrFail($id); if ($phone->status === 'converted') { abort(403, 'Unauthorized action.'); } $users = User::forDropdown($business_id, false, false, false, true); $statuses = CrmPhoneFollowUp::statusDropdown(); return view('crm::phone_list.edit')->with(compact('phone', 'users', 'statuses')); } public function update(Request $request, $id) { $this->authorizeAccess(); $business_id = request()->session()->get('user.business_id'); try { $phone = $this->baseQuery($business_id)->findOrFail($id); if ($phone->status === 'converted') { return ['success' => false, 'msg' => __('crm::lang.phone_already_converted')]; } $request->validate([ 'mobile_number' => 'required|string|max:191', 'contact_name' => 'nullable|string|max:191', 'assigned_to' => 'nullable|integer', 'notes' => 'nullable|string', 'status' => 'required|in:pending,contacted,no_answer,interested,not_interested', ]); $phone->update($request->only(['mobile_number', 'contact_name', 'assigned_to', 'notes', 'status'])); return ['success' => true, 'msg' => __('lang_v1.success')]; } catch (\Exception $e) { \Log::emergency('File:'.$e->getFile().'Line:'.$e->getLine().'Message:'.$e->getMessage()); return ['success' => false, 'msg' => __('messages.something_went_wrong')]; } } public function destroy($id) { $this->authorizeAccess(); $business_id = request()->session()->get('user.business_id'); try { $phone = $this->baseQuery($business_id)->findOrFail($id); if ($phone->status === 'converted') { return ['success' => false, 'msg' => __('crm::lang.phone_already_converted')]; } $phone->delete(); return ['success' => true, 'msg' => __('lang_v1.deleted_success')]; } catch (\Exception $e) { \Log::emergency('File:'.$e->getFile().'Line:'.$e->getLine().'Message:'.$e->getMessage()); return ['success' => false, 'msg' => __('messages.something_went_wrong')]; } } public function showFollowUpForm($id) { $this->authorizeAccess(); $business_id = request()->session()->get('user.business_id'); $phone = $this->baseQuery($business_id)->findOrFail($id); $statuses = CrmPhoneFollowUp::statusDropdown(); unset($statuses['converted']); return view('crm::phone_list.follow_up_modal')->with(compact('phone', 'statuses')); } public function storeFollowUp(Request $request, $id) { $this->authorizeAccess(); $business_id = request()->session()->get('user.business_id'); try { $phone = $this->baseQuery($business_id)->findOrFail($id); if ($phone->status === 'converted') { return ['success' => false, 'msg' => __('crm::lang.phone_already_converted')]; } $request->validate([ 'status' => 'required|in:pending,contacted,no_answer,interested,not_interested', 'note' => 'nullable|string', ]); $user_id = $request->session()->get('user.id'); CrmPhoneFollowUpLog::create([ 'phone_follow_up_id' => $phone->id, 'status' => $request->input('status'), 'note' => $request->input('note'), 'created_by' => $user_id, ]); $phone->update([ 'status' => $request->input('status'), 'last_follow_up_at' => now(), 'follow_up_count' => $phone->follow_up_count + 1, ]); return ['success' => true, 'msg' => __('crm::lang.follow_up_logged')]; } catch (\Exception $e) { \Log::emergency('File:'.$e->getFile().'Line:'.$e->getLine().'Message:'.$e->getMessage()); return ['success' => false, 'msg' => __('messages.something_went_wrong')]; } } public function showConvertForm($id) { $this->authorizeAccess(); $business_id = request()->session()->get('user.business_id'); $phone = $this->baseQuery($business_id)->findOrFail($id); if ($phone->status === 'converted') { abort(403, 'Unauthorized action.'); } $users = User::forDropdown($business_id, false, false, false, true); $sources = Category::forDropdown($business_id, 'source'); $life_stages = Category::forDropdown($business_id, 'life_stage'); $name_parts = $this->splitContactName($phone->contact_name); return view('crm::phone_list.convert_to_lead_modal')->with(compact( 'phone', 'users', 'sources', 'life_stages', 'name_parts' )); } public function convertToLead(Request $request, $id) { $this->authorizeAccess(); $business_id = request()->session()->get('user.business_id'); try { $phone = $this->baseQuery($business_id)->findOrFail($id); if ($phone->status === 'converted') { return ['success' => false, 'msg' => __('crm::lang.phone_already_converted')]; } $request->validate([ 'first_name' => 'required|string|max:191', 'mobile' => 'required|string|max:191', 'crm_source' => 'nullable|integer', 'crm_life_stage' => 'nullable|integer', ]); $input = $request->only([ 'prefix', 'first_name', 'middle_name', 'last_name', 'mobile', 'landline', 'alternate_number', 'email', 'supplier_business_name', 'crm_source', 'crm_life_stage', 'crm_lead_title', 'crm_lead_description', 'city', 'state', 'country', 'address_line_1', 'zip_code', ]); $input['type'] = 'lead'; $input['name'] = implode(' ', array_filter([ $input['prefix'] ?? '', $input['first_name'] ?? '', $input['middle_name'] ?? '', $input['last_name'] ?? '', ])); $input['business_id'] = $business_id; $input['created_by'] = $request->session()->get('user.id'); if ($request->filled('crm_lead_value')) { $input['crm_lead_value'] = $this->commonUtil->num_uf($request->input('crm_lead_value')); } $assigned_to = $request->input('user_id', []); if (empty($assigned_to) && ! empty($phone->assigned_to)) { $assigned_to = [$phone->assigned_to]; } $lead = CrmContact::createNewLead($input, $assigned_to); CrmPhoneFollowUpLog::create([ 'phone_follow_up_id' => $phone->id, 'status' => 'converted', 'note' => __('crm::lang.converted_to_lead_note'), 'created_by' => $request->session()->get('user.id'), ]); $phone->update([ 'status' => 'converted', 'lead_id' => $lead->id, 'last_follow_up_at' => now(), 'follow_up_count' => $phone->follow_up_count + 1, ]); return [ 'success' => true, 'msg' => __('crm::lang.converted_to_lead_success'), 'lead_url' => action([LeadController::class, 'show'], ['lead' => $lead->id]), ]; } catch (\Exception $e) { \Log::emergency('File:'.$e->getFile().'Line:'.$e->getLine().'Message:'.$e->getMessage()); return ['success' => false, 'msg' => __('messages.something_went_wrong')]; } } public function import() { $this->authorizeAccess(); return view('crm::phone_list.import'); } public function downloadTemplate() { $this->authorizeAccess(); $headers = [ __('lang_v1.contact_no'), __('contact.name'), __('crm::lang.import_col_company_name'), __('crm::lang.import_col_city'), __('crm::lang.description'), __('crm::lang.assgined').' ('.__('lang_v1.optional').')', ]; $callback = function () use ($headers) { $file = fopen('php://output', 'w'); fprintf($file, chr(0xEF).chr(0xBB).chr(0xBF)); fputcsv($file, $headers); fputcsv($file, ['09123456789', 'علی رضایی', 'کارخانه نمونه', 'تهران', 'تماس اولیه', '']); fclose($file); }; return response()->streamDownload($callback, 'phone_list_import_template.csv', [ 'Content-Type' => 'text/csv; charset=UTF-8', ]); } public function postImport(Request $request) { $this->authorizeAccess(); $business_id = request()->session()->get('user.business_id'); $user_id = request()->session()->get('user.id'); try { ini_set('max_execution_time', 0); $request->validate([ 'phone_list_file' => 'required|file|mimes:csv,txt,xls,xlsx', ]); $file = $request->file('phone_list_file'); $parsed_array = Excel::toArray([], $file); $imported_data = array_splice($parsed_array[0], 1); $created = 0; $skipped = 0; $errors = []; \DB::beginTransaction(); foreach ($imported_data as $key => $value) { $row_no = $key + 2; $mobile = trim($value[0] ?? ''); if (empty($mobile)) { continue; } $result = $this->phoneListUtil->createFromImportRow($business_id, $user_id, [ 'mobile_number' => $mobile, 'contact_name' => trim($value[1] ?? '') ?: null, 'company_name' => trim($value[2] ?? '') ?: null, 'city' => trim($value[3] ?? '') ?: null, 'notes' => trim($value[4] ?? '') ?: null, 'assigned_to' => trim($value[5] ?? '') ?: null, ]); if ($result['status'] === 'created') { $created++; } elseif ($result['status'] === 'skipped') { $skipped++; } else { $errors[] = __('crm::lang.import_row_error', ['row' => $row_no]); } } \DB::commit(); $msg = __('crm::lang.phone_list_import_result', ['created' => $created, 'skipped' => $skipped]); if (! empty($errors)) { $msg .= ' '.implode(' ', array_slice($errors, 0, 3)); } return redirect() ->action([self::class, 'import']) ->with('status', ['success' => 1, 'msg' => $msg]); } catch (\Exception $e) { \DB::rollBack(); \Log::emergency('File:'.$e->getFile().'Line:'.$e->getLine().'Message:'.$e->getMessage()); return redirect() ->action([self::class, 'import']) ->with('status', ['success' => 0, 'msg' => $e->getMessage()]); } } private function splitContactName(?string $name): array { $parts = ['first_name' => '', 'last_name' => '']; if (empty($name)) { return $parts; } $name = trim($name); $segments = preg_split('/\s+/', $name, 2); $parts['first_name'] = $segments[0] ?? ''; $parts['last_name'] = $segments[1] ?? ''; return $parts; } }