route())->getName(); return is_string($routeName) && Str::startsWith($routeName, 'leads.leads.'); } private static function customIndexFields() { if (!self::shouldIncludeCustomIndexColumns()) { return []; } $service = app(LeadFieldSettingsService::class); $customFields = $service->getVisibleCustomFieldsForIndex(Auth::user()); $result = []; foreach ($customFields as $field) { $key = (string) ($field['key'] ?? ''); if ($key === '' || !preg_match('/^[A-Za-z0-9_]+$/', $key)) { continue; } $column = self::CUSTOM_COLUMN_PREFIX . $key; $result[$column] = [ 'column' => $column, 'key' => $key, 'label' => (string) ($field['label'] ?? Str::title(str_replace('_', ' ', $key))), 'type' => (string) ($field['type'] ?? 'text'), 'uses_column' => !empty($field['uses_column']), ]; } return $result; } private static function customIndexColumns() { $columns = []; foreach (self::customIndexFields() as $field) { $dataType = 'text'; if ($field['type'] === 'email') { $dataType = 'email'; } elseif ($field['type'] === 'date') { $dataType = 'date'; } elseif ($field['type'] === 'datetime') { $dataType = 'datetime'; } $columns[$field['column']] = [ 'data' => $field['column'], 'title' => $field['label'], 'data_type' => $dataType, 'filter_type' => DataTableHelper::isDateCustomFieldType($field['type']) ? 'bap_date_range_picker' : 'text', 'searchable' => true, ]; } return $columns; } public static function availableQueryFilters() { $labelMap = []; if (Auth::check() && Schema::hasTable('lead_user_field_settings')) { $service = app(LeadFieldSettingsService::class); $labelMap = $service->getFieldLabelMap(Auth::user()); } $getLabel = function ($fieldKey, $defaultTranslation) use ($labelMap) { return $labelMap[$fieldKey] ?? trans($defaultTranslation); }; return [ [ 'id' => 'leads.full_name', 'label' => $getLabel('full_name', 'leads::leads.form.full_name'), 'type' => 'string', ], [ 'id' => 'lead_email.email', 'label' => $getLabel('email', 'leads::leads.form.email'), 'type' => 'string', ], [ 'id' => 'leads.capture_date', 'label' => $getLabel('capture_date', 'leads::leads.form.capture_date'), 'type' => 'date', 'input_event' => 'dp.change', 'plugin' => 'datetimepicker', 'plugin_config' => [ 'locale' => app()->getLocale(), 'calendarWeeks' => true, 'showClear' => true, 'showTodayButton' => true, 'showClose' => true, 'format' => \Modules\Platform\Core\Helper\UserHelper::userJsDateFormat() ] ], [ 'id' => 'leads.birth_date', 'label' => $getLabel('birth_date', 'leads::leads.form.birth_date'), 'type' => 'date', 'input_event' => 'dp.change', 'plugin' => 'datetimepicker', 'plugin_config' => [ 'locale' => app()->getLocale(), 'calendarWeeks' => true, 'showClear' => true, 'showTodayButton' => true, 'showClose' => true, 'format' => \Modules\Platform\Core\Helper\UserHelper::userJsDateFormat() ] ], [ 'id' => 'leads.secondary_email', 'label' => $getLabel('secondary_email', 'leads::leads.form.secondary_email'), 'type' => 'string' ], [ 'id' => 'leads.lead_company', 'label' => $getLabel('lead_company', 'leads::leads.form.company'), 'type' => 'string' ], [ 'id' => 'leads.created_at', 'label' => $getLabel('created_at', 'leads::leads.form.created_at'), 'type' => 'date', 'input_event' => 'dp.change', 'plugin' => 'datetimepicker', 'plugin_config' => [ 'locale' => app()->getLocale(), 'calendarWeeks' => true, 'showClear' => true, 'showTodayButton' => true, 'showClose' => true, 'format' => \Modules\Platform\Core\Helper\UserHelper::userJsDateFormat() ] ], [ 'id' => 'leads.updated_at', 'label' => $getLabel('updated_at', 'leads::leads.form.updated_at'), 'type' => 'date', 'input_event' => 'dp.change', 'plugin' => 'datetimepicker', 'plugin_config' => [ 'locale' => app()->getLocale(), 'calendarWeeks' => true, 'showClear' => true, 'showTodayButton' => true, 'showClose' => true, 'format' => \Modules\Platform\Core\Helper\UserHelper::userJsDateFormat() ] ], [ 'id' => 'leads.annual_revenue', 'label' => $getLabel('annual_revenue', 'leads::leads.form.annual_revenue'), 'type' => 'double', ], [ 'id' => 'leads.no_of_employees', 'label' => $getLabel('no_of_employees', 'leads::leads.form.no_of_employees'), 'type' => 'integer', ], [ 'id' => 'leads.phone', 'label' => $getLabel('phone', 'leads::leads.form.phone'), 'type' => 'string' ], [ 'id' => 'leads.mobile', 'label' => $getLabel('mobile', 'leads::leads.form.mobile'), 'type' => 'string' ], [ 'id' => 'leads.fax', 'label' => $getLabel('fax', 'leads::leads.form.fax'), 'type' => 'string' ], [ 'id' => 'leads.twitter', 'label' => $getLabel('twitter', 'leads::leads.form.twitter'), 'type' => 'string' ], [ 'id' => 'leads.facebook', 'label' => $getLabel('facebook', 'leads::leads.form.facebook'), 'type' => 'string' ], [ 'id' => 'leads.skype', 'label' => $getLabel('skype', 'leads::leads.form.skype'), 'type' => 'string' ], [ 'id' => 'leads.website', 'label' => $getLabel('website', 'leads::leads.form.website'), 'type' => 'string' ], [ 'id' => 'leads.addr_street', 'label' => $getLabel('addr_street', 'leads::leads.form.addr_street'), 'type' => 'string' ] , [ 'id' => 'leads.addr_state', 'label' => $getLabel('addr_state', 'leads::leads.form.addr_state'), 'type' => 'string' ], [ 'id' => 'leads.addr_country_id', 'label' => $getLabel('addr_country_id', 'leads::leads.form.addr_country_id'), 'type' => 'integer', 'input' => 'select', 'multiple' => true, 'plugin' => 'select2', 'plugin_config' => [ 'multiple' => 'multiple', 'width' => '300px', ], 'values' => Country::pluck('name', 'id'), 'operators' => [ 'in', 'not_in', 'is_null', 'is_not_null' ] ], [ 'id' => 'bap_country.continent', 'label' => $getLabel('continent', 'leads::leads.form.continent'), 'type' => 'string', 'input' => 'select', 'multiple' => true, 'plugin' => 'select2', 'plugin_config' => [ 'multiple' => 'multiple', 'width' => '300px', ], 'values' => Country::pluck('continent', 'continent'), 'operators' => [ 'in', 'not_in', 'is_null', 'is_not_null' ] ], [ 'id' => 'leads.addr_city', 'label' => $getLabel('addr_city', 'leads::leads.form.addr_city'), 'type' => 'string' ], [ 'id' => 'leads.addr_zip', 'label' => $getLabel('addr_zip', 'leads::leads.form.addr_zip'), 'type' => 'string' ], [ 'id' => 'leads.job_title', 'label' => $getLabel('job_title', 'leads::leads.form.job_title'), 'type' => 'string' ], [ 'id' => 'leads.description', 'label' => $getLabel('description', 'leads::leads.form.description'), 'type' => 'string' ], [ 'id' => 'leads.lead_status_id', 'label' => $getLabel('lead_status_id', 'leads::leads.form.lead_status_id'), 'type' => 'integer', 'input' => 'select', 'multiple' => true, 'plugin' => 'select2', 'plugin_config' => [ 'multiple' => 'multiple', 'width' => '300px', ], 'values' => LeadStatus::pluck('name', 'id'), 'operators' => [ 'in', 'not_in', 'is_null', 'is_not_null' ] ], [ 'id' => 'leads.language_id', 'label' => $getLabel('language_id', 'leads::leads.form.language_id'), 'type' => 'integer', 'input' => 'select', 'multiple' => true, 'plugin' => 'select2', 'plugin_config' => [ 'multiple' => 'multiple', 'width' => '300px', ], 'values' => CustomerLanguage::pluck('name', 'id'), 'operators' => [ 'in', 'not_in', 'is_null', 'is_not_null' ] ], [ 'id' => 'leads.lead_source_id', 'label' => $getLabel('lead_source_id', 'leads::leads.form.lead_source_id'), 'type' => 'integer', 'input' => 'select', 'multiple' => true, 'plugin' => 'select2', 'plugin_config' => [ 'multiple' => 'multiple', 'width' => '300px', ], 'values' => LeadSource::pluck('name', 'id'), 'operators' => [ 'in', 'not_in', 'is_null', 'is_not_null' ] ], [ 'id' => 'leads.lead_industry_id', 'label' => $getLabel('lead_industry_id', 'leads::leads.form.lead_industry_id'), 'type' => 'integer', 'input' => 'select', 'multiple' => true, 'plugin' => 'select2', 'plugin_config' => [ 'multiple' => 'multiple', 'width' => '300px', ], 'values' => LeadIndustry::pluck('name', 'id'), 'operators' => [ 'in', 'not_in', 'is_null', 'is_not_null' ] ], [ 'id' => 'leads.lead_rating_id', 'label' => $getLabel('lead_rating_id', 'leads::leads.form.lead_rating_id'), 'type' => 'integer', 'input' => 'select', 'multiple' => true, 'plugin' => 'select2', 'plugin_config' => [ 'multiple' => 'multiple', 'width' => '300px', ], 'values' => LeadRating::pluck('name', 'id'), 'operators' => [ 'in', 'not_in', 'is_null', 'is_not_null' ] ], [ 'id' => 'lead_multi_select.multi_select_id', 'label' => $getLabel('multiSelects', 'leads::leads.form.multiSelects'), 'type' => 'string', 'input' => 'select', 'multiple' => true, 'plugin' => 'select2', 'plugin_config' => [ 'multiple' => 'multiple', 'width' => '300px', ], 'values' => LeadMultiSelect::pluck('name', 'id'), 'operators' => [ 'in', 'not_in', 'is_null', 'is_not_null' ] ], ]; } /** * Maps datatable column keys to their corresponding field settings keys * when the names differ (e.g. 'source' column → 'lead_source_id' field). */ private static $columnToFieldMap = [ 'source' => 'lead_source_id', 'status' => 'lead_status_id', 'industry' => 'lead_industry_id', 'rating' => 'lead_rating_id', 'multiSelects' => 'multiSelects', 'owner' => 'owned_by', 'addr_country' => 'addr_country_id', 'language' => 'language_id', ]; /** * Resolve the field settings key for a given datatable column key. */ private static function fieldKeyForColumn($columnKey) { return self::$columnToFieldMap[$columnKey] ?? $columnKey; } public static function availableColumns() { if(auth()->user()->cannot('crm.advanced')){ $allColumns = [ 'full_name' => [ 'data' => 'full_name', 'title' => trans('leads::leads.form.name'), 'data_type' => 'text', 'filter_type' => 'text', ], 'phone' => [ 'data' => 'phone', 'title' => trans('leads::leads.form.phone'), 'data_type' => 'text', 'filter_type' => 'text' ], 'mobile' => [ 'data' => 'mobile', 'title' => trans('leads::leads.form.mobile'), 'data_type' => 'text', 'filter_type' => 'text' ], 'created_at' => [ 'data' => 'created_at', 'title' => trans('leads::leads.form.created_at'), 'data_type' => 'datetime', 'filter_type' => 'bap_date_range_picker', ], 'owner' => [ 'data' => 'owner', 'title' => trans('core::core.table.assigned_to'), 'data_type' => 'assigned_to', 'orderable' => false, 'column_name' => 'assigned_to', 'filter_type' => 'select', 'select_type' => 'select2', 'select_type_options' => [ 'theme' => "bootstrap", 'width' => '100%' ], 'filter_data' => DataTableHelper::filterOwnerDropdown() ] ]; }else{ $allColumns = [ 'id' => [ 'data' => 'id', 'title' => trans('leads::leads.form.id'), 'data_type' => 'text', 'filter_type' => 'text', ], 'full_name' => [ 'data' => 'full_name', 'title' => trans('leads::leads.form.name'), 'data_type' => 'text', 'filter_type' => 'text', ], 'phone' => [ 'data' => 'phone', 'title' => trans('leads::leads.form.phone'), 'data_type' => 'text', 'filter_type' => 'text' ], 'mobile' => [ 'data' => 'mobile', 'title' => trans('leads::leads.form.mobile'), 'data_type' => 'text', 'filter_type' => 'text' ], 'email' => [ 'data' => 'email', 'title' => trans('leads::leads.form.email'), 'data_type' => 'text', 'filter_type' => 'text' ], 'capture_date' => [ 'data' => 'capture_date', 'title' => trans('leads::leads.form.capture_date'), 'data_type' => 'datetime', 'filter_type' => 'bap_date_range_picker' ], 'birth_date' => [ 'data' => 'birth_date', 'title' => trans('leads::leads.form.birth_date'), 'data_type' => 'date', 'filter_type' => 'bap_date_range_picker' ], 'source' => [ 'name' => 'leads_dict_source.name', 'data' => 'source', 'title' => trans('leads::leads.form.source'), 'data_type' => 'text', 'filter_type' => 'text' ], 'status' => [ 'name' => 'leads_dict_status.name', 'data' => 'status', 'title' => trans('leads::leads.form.status'), 'data_type' => 'text', 'filter_type' => 'text', 'column_name' => 'lead_status_id' ], 'industry' => [ 'name' => 'leads_dict_industry.name', 'data' => 'industry', 'title' => trans('leads::leads.form.industry'), 'data_type' => 'text', 'filter_type' => 'text' ], 'rating' => [ 'name' => 'leads_dict_rating.name', 'data' => 'rating', 'title' => trans('leads::leads.form.rating'), 'data_type' => 'text', 'filter_type' => 'text' ], 'multiSelects' => [ 'data' => 'multi_selects_text', 'title' => trans('leads::leads.form.multiSelects'), 'data_type' => 'text', 'filter_type' => 'text', 'orderable' => false, 'searchable' => true, ], 'lead_company' => [ 'data' => 'lead_company', 'title' => trans('leads::leads.form.company'), 'data_type' => 'text', 'filter_type' => 'text' ], 'secondary_email' => [ 'data' => 'secondary_email', 'title' => trans('leads::leads.form.secondary_email'), 'data_type' => 'text', 'filter_type' => 'text' ], 'annual_revenue' => [ 'data' => 'annual_revenue', 'title' => trans('leads::leads.form.annual_revenue'), 'data_type' => 'text', 'filter_type' => 'text' ], 'no_of_employees' => [ 'data' => 'no_of_employees', 'title' => trans('leads::leads.form.no_of_employees'), 'data_type' => 'text', 'filter_type' => 'text' ], 'fax' => [ 'data' => 'fax', 'title' => trans('leads::leads.form.fax'), 'data_type' => 'text', 'filter_type' => 'text' ], 'twitter' => [ 'data' => 'twitter', 'title' => trans('leads::leads.form.twitter'), 'data_type' => 'text', 'filter_type' => 'text' ], 'facebook' => [ 'data' => 'facebook', 'title' => trans('leads::leads.form.facebook'), 'data_type' => 'text', 'filter_type' => 'text' ], 'skype' => [ 'data' => 'skype', 'title' => trans('leads::leads.form.skype'), 'data_type' => 'text', 'filter_type' => 'text' ], 'website' => [ 'data' => 'website', 'title' => trans('leads::leads.form.website'), 'data_type' => 'text', 'filter_type' => 'text' ], 'language' => [ 'name' => 'customer_dict_language.name', 'data' => 'language', 'title' => trans('leads::leads.form.language'), 'data_type' => 'text', 'filter_type' => 'text' ], 'addr_street' => [ 'data' => 'addr_street', 'title' => trans('leads::leads.form.addr_street'), 'data_type' => 'text', 'filter_type' => 'text' ], 'addr_state' => [ 'data' => 'addr_state', 'title' => trans('leads::leads.form.addr_state'), 'data_type' => 'text', 'filter_type' => 'text' ], 'addr_country' => [ 'data' => 'addr_country', 'name' => 'bap_country.name', 'title' => trans('leads::leads.form.addr_country_id'), 'data_type' => 'text', 'filter_type' => 'text' ], 'addr_city' => [ 'data' => 'addr_city', 'title' => trans('leads::leads.form.addr_city'), 'data_type' => 'text', 'filter_type' => 'text' ], 'addr_zip' => [ 'data' => 'addr_zip', 'title' => trans('leads::leads.form.addr_zip'), 'data_type' => 'text', 'filter_type' => 'text' ], 'job_title' => [ 'data' => 'job_title', 'title' => trans('leads::leads.form.job_title'), 'data_type' => 'text', 'filter_type' => 'text' ], 'description' => [ 'data' => 'description', 'title' => trans('leads::leads.form.description'), 'data_type' => 'text', 'filter_type' => 'text' ], 'created_at' => [ 'data' => 'created_at', 'title' => trans('leads::leads.form.created_at'), 'data_type' => 'datetime', 'filter_type' => 'bap_date_range_picker', ], 'updated_at' => [ 'data' => 'updated_at', 'title' => trans('leads::leads.form.updated_at'), 'data_type' => 'datetime', 'filter_type' => 'bap_date_range_picker', ], 'owner' => [ 'data' => 'owner', 'title' => trans('core::core.table.assigned_to'), 'data_type' => 'assigned_to', 'orderable' => false, 'column_name' => 'assigned_to', 'filter_type' => 'select', 'select_type' => 'select2', 'select_type_options' => [ 'theme' => "bootstrap", 'width' => '100%' ], 'filter_data' => DataTableHelper::filterOwnerDropdown() ] ]; } $allColumns = $allColumns + self::customIndexColumns(); // Filter columns to only show those that are visible in the user's field settings if (Auth::check() && Schema::hasTable('lead_user_field_settings')) { $service = app(LeadFieldSettingsService::class); $visibleKeys = $service->getVisibleFieldKeys(Auth::user()); $orderMap = $service->getFieldOrderMap(Auth::user()); $labelMap = $service->getFieldLabelMap(Auth::user()); // Always-visible columns not managed by field settings $alwaysVisible = ['id', 'created_at', 'updated_at']; // full_name column shows if any name field is visible $nameFieldVisible = !empty(array_intersect(['first_name', 'last_name', 'full_name'], $visibleKeys)); $filteredColumns = []; foreach ($allColumns as $colKey => $colDef) { // Custom columns (prefixed) are already filtered by getVisibleCustomFieldsForIndex if (Str::startsWith($colKey, self::CUSTOM_COLUMN_PREFIX)) { $filteredColumns[$colKey] = $colDef; continue; } if (in_array($colKey, $alwaysVisible, true)) { $filteredColumns[$colKey] = $colDef; continue; } // Special case: full_name shown if any name-related field is visible if ($colKey === 'full_name' && $nameFieldVisible) { $labelKey = Auth::user()->can('crm.advanced') ? 'full_name' : 'name'; if (isset($labelMap[$labelKey])) { $colDef['title'] = $labelMap[$labelKey]; } $filteredColumns[$colKey] = $colDef; continue; } $fieldKey = self::fieldKeyForColumn($colKey); if (in_array($fieldKey, $visibleKeys, true)) { if (isset($labelMap[$fieldKey])) { $colDef['title'] = $labelMap[$fieldKey]; } $filteredColumns[$colKey] = $colDef; } } // Sort columns by field order from settings $customPrefix = self::CUSTOM_COLUMN_PREFIX; uksort($filteredColumns, function ($a, $b) use ($orderMap, $customPrefix) { // Pin id first, created_at/updated_at last $pinFirst = ['id' => -2]; $pinLast = ['created_at' => 999990, 'updated_at' => 999991]; if (isset($pinFirst[$a]) || isset($pinLast[$a])) { $orderA = $pinFirst[$a] ?? $pinLast[$a]; } else { $fieldA = self::$columnToFieldMap[$a] ?? $a; if ($fieldA === 'full_name') $fieldA = 'first_name'; // Strip custom__ prefix so custom fields resolve to their settings key if (strpos($fieldA, $customPrefix) === 0) { $fieldA = substr($fieldA, strlen($customPrefix)); } $orderA = $orderMap[$fieldA] ?? 99999; } if (isset($pinFirst[$b]) || isset($pinLast[$b])) { $orderB = $pinFirst[$b] ?? $pinLast[$b]; } else { $fieldB = self::$columnToFieldMap[$b] ?? $b; if ($fieldB === 'full_name') $fieldB = 'first_name'; if (strpos($fieldB, $customPrefix) === 0) { $fieldB = substr($fieldB, strlen($customPrefix)); } $orderB = $orderMap[$fieldB] ?? 99999; } return $orderA <=> $orderB; }); return $filteredColumns; } return $allColumns; } protected function setFilterDefinition() { $filters = self::availableQueryFilters(); // Enforce show_cid: hide phone/mobile query-builder filters for users without the flag. if (Auth::check() && ! Auth::user()->show_cid) { $filters = array_values(array_filter($filters, function ($f) { return ! in_array($f['id'] ?? '', ['leads.phone', 'leads.mobile'], true); })); } $this->filterDefinition = $filters; } /** * Build DataTable class. * * @param mixed $query Results from query() method. * @return \Yajra\DataTables\DataTableAbstract */ public function dataTable($query) { $dataTable = new EloquentDataTable($query); $this->applyLinks($dataTable, self::SHOW_URL_ROUTE); $dataTable->filterColumn('owner', [self::class, 'applyOwnerFilter']); // $dataTable->editColumn('status', function ($record) { // $text = "$record->status"; // return DataTableHelper::applyLink($text,route(self::SHOW_URL_ROUTE,$record->id)); // }); $dataTable->editColumn('email', function ($record) { return $record->email; }); $dataTable->editColumn('full_name', function ($record) { return '
'; }); $dataTable->editColumn('description', function ($record) { return ''; }); $dataTable->filterColumn('email', [self::class, 'applyEmailFilter']); foreach ([ 'created_at' => 'leads.created_at', 'updated_at' => 'leads.updated_at', 'capture_date' => 'leads.capture_date', 'birth_date' => 'leads.birth_date', ] as $column => $sqlColumn) { $dataTable->filterColumn($column, function ($query, $keyword) use ($sqlColumn) { self::applyDateRangeFilter($query, $sqlColumn, $keyword); }); } // One pass over the pivot table instead of an EXISTS sub query that is // evaluated for every lead (the pivot has no index on lead_id). $dataTable->filterColumn('multi_selects_text', [self::class, 'applyMultiSelectsFilter']); foreach (self::customIndexFields() as $field) { $column = $field['column']; $dataTable->filterColumn($column, function ($query, $keyword) use ($field) { self::applyCustomFieldFilter($query, $field, $keyword); }); } // Multi selects: rendered from the eager loaded relation. Doing it per // row also used to trigger one relation query per row (N+1). $dataTable->addColumn('multi_selects_text', function ($record) { return self::multiSelectsText($record); }); // The "multiSelects" cell (rendered by applyLinks()) would otherwise // embed the whole relation as json inside the cell title attribute. $dataTable->editColumn('multiSelects', function ($record) { return self::multiSelectsText($record); }); // Keep the JSON payload small: the relations are only needed server // side to render the cells, serializing them for every row produces // several KB of unused data per record. $dataTable->makeHidden(['owner', 'owned_by', 'multiSelects', 'leadEmails']); // Enforce show_cid: null out phone/mobile in the JSON payload for users without the flag. if (Auth::check() && ! Auth::user()->show_cid) { $dataTable->editColumn('phone', function () { return null; }); $dataTable->editColumn('mobile', function () { return null; }); } return $dataTable; } /** * Filter a query by owner name/keyword. */ public static function applyOwnerFilter($query, $keyword) { DataTableHelper::queryOwner($query, $keyword, 'leads'); } /** * Filter a query by (multi) email address. * * The matching lead ids are resolved first: with a concrete id list the * listing can use the created_at index, while a correlated EXISTS / IN * sub query forces a rescan of the whole lead_email table for the page * query. The lookup is memoized, so a chunked export resolves it once. */ public static function applyEmailFilter($query, $keyword, $boolean = 'and') { $leadIds = self::filterLeadIds('email|' . $keyword, function () use ($keyword) { return \DB::table('lead_email') ->where('lead_email.email', 'LIKE', '%' . $keyword . '%') ->pluck('lead_email.lead_id') ->all(); }); self::applyIdListCondition($query, 'leads.id', $leadIds, $boolean); } /** * Apply a "