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