'leads.browse', 'create' => 'leads.create', 'update' => 'leads.update', 'destroy' => 'leads.destroy' ]; protected $jsFiles = [ 'BAP_Leads.js' ]; protected $moduleSettingsLinks = [ ['route' => 'leads.status.index', 'label' => 'settings.status'], ['route' => 'leads.source.index', 'label' => 'settings.source'], ['route' => 'leads.rating.index', 'label' => 'settings.rating'], ['route' => 'leads.industry.index', 'label' => 'settings.industry'], ['route' => 'leads.multiSelect.index', 'label' => 'settings.multiSelect'], ]; protected function indexActionButtons() { $noPermissionIsRequired = (array_key_exists('create', $this->permissions) && $this->permissions['create'] == ''); $hasPermissionifRequired = (isset($this->permissions['create']) && $this->permissions['create'] != '' && \Auth::user()->hasPermissionTo($this->permissions['create'])); if ($noPermissionIsRequired || $hasPermissionifRequired) { if(auth()->user()->can('leads.report')){ $this->indexActionButtons[] = array( 'href' => route('leads.leads.report'), 'attr' => [ 'class' => ['Report'] ], 'label' => 'Analytics' ); } $this->indexActionButtons[] = array( 'href' => '#', 'attr' => [ 'class' => ['quick_create'] ], 'label' => trans('core::core.btn.quick_create') ); if (isset($this->routes['import'])) { $this->indexActionButtons[] = array( 'href' => '#', 'attr' => [ 'class' => ['records_import'] ], 'label' => trans('core::core.btn.import') ); } } } protected function setupActionButtons() { $this->actionButtons[] = array( 'href' => route($this->routes['create'], ['copy' => $this->entity->id]), 'attr' => [ ], 'label' => trans('core::core.btn.copy') ); if(auth()->user()->can('crm.advanced')){ $this->actionButtons[] = array( 'href' => route('leads.leads.convert.to.contact', ['id' => $this->entity->id]), 'attr' => [ ], 'label' => trans('leads::leads.convert_to_contact') ); } } protected function setupAssignedTo($entity, $input, $creating = false) { if ($creating) { // Suppress the follow-up save() in ModuleCrudController::store() that // persists owner bookkeeping right after creation. Spatie's LogsActivity // would otherwise log a spurious 'updated' entry for every create. $entity->disableLogging(); } if (!auth()->user()->hasPermissionTo('leads.assign')) { return $entity; } return parent::setupAssignedTo($entity, $input, $creating); } /** * Return every lead id that matches the current listing state. * * This is used by the "select all" bulk action so the delete flow can * operate on all filtered records across every data table page without * changing the existing destroy/forceDelete handler. * * @param Request $request * @return \Illuminate\Http\JsonResponse */ public function selectionIds(Request $request) { if ($this->permissions['browse'] != '' && !\Auth::user()->hasPermissionTo($this->permissions['browse'])) { return response()->json([ 'type' => 'error', 'message' => trans('core::core.you_dont_have_access'), 'action' => 'show_message', ], 403); } $datatable = \App::make(LeadsDatatable::class); // The listing only shows the leads of the current user (or of the // user's groups) unless the user has access to all entities. Only // those records may be selected by "select all". if ($this->scopedAccess() && !$this->moduleDictionary) { $datatable->addScope(new OwnableEntityScope(\Auth::user(), $this->moduleName, $this->entityClass)); } $query = $datatable->exportQuery($request); $ids = $query ->select('leads.id') ->pluck('leads.id') ->map(function ($id) { return (int) $id; }) ->values() ->all(); return response()->json([ 'type' => 'success', 'ids' => $ids, ]); } /** * Stream all (filtered) leads as a CSV file. * * Rows are read in chunks and written straight to the response, so * exporting 100k+ leads keeps a flat memory footprint and never blocks * the lead tables for other users (no long running transaction, no * "SELECT *" of the whole table at once). * * The filters of the listing (advanced query rules, column filters and * the search box) are honoured, the request parameters are the ones * DataTables sent for the current listing state. * * @param Request $request * @return \Symfony\Component\HttpFoundation\StreamedResponse|\Illuminate\Http\RedirectResponse */ public function export(Request $request) { if ($this->permissions['browse'] != '' && !\Auth::user()->hasPermissionTo($this->permissions['browse'])) { flash(trans('core::core.you_dont_have_access'))->error(); return redirect()->route($this->routes['index']); } $datatable = \App::make(LeadsDatatable::class); $columns = LeadsDatatable::csvExportColumns(); $query = $datatable->exportQuery($request); $filename = 'leads-' . date('Y-m-d-His') . '.csv'; return response()->streamDownload(function () use ($query, $columns) { @set_time_limit(0); $handle = fopen('php://output', 'w'); // UTF-8 BOM so Excel detects the encoding correctly. fwrite($handle, "\xEF\xBB\xBF"); fputcsv($handle, array_map(function ($column) { return $column['title'] ?? ($column['data'] ?? ''); }, $columns)); $query->chunkById(LeadsDatatable::EXPORT_CHUNK_SIZE, function ($leads) use ($handle, $columns) { foreach ($leads as $lead) { fputcsv($handle, array_map(function ($column) use ($lead) { return LeadsDatatable::exportValue($lead, $column); }, $columns)); } // Push every chunk to the client right away. if (ob_get_level() > 0) { @ob_flush(); } @flush(); }, 'leads.id', 'id'); fclose($handle); }, $filename, [ 'Content-Type' => 'text/csv; charset=UTF-8', 'Cache-Control' => 'no-store, no-cache, must-revalidate', ]); } public function convertToContact($leadId) { if ($this->permissions['create'] != '' && !\Auth::user()->hasPermissionTo($this->permissions['create'])) { flash(trans('core::core.you_dont_have_access'))->error(); return redirect()->route($this->routes['index']); } $leadService = \App::make(LeadService::class); $contact = $leadService->convertToContact($leadId); if (!empty($contact)) { flash(trans('core::core.record_converted'))->success(); return redirect()->route('contacts.contacts.show', $contact->id); } flash(trans('core::core.error_while_converting'))->error(); return redirect()->route($this->routes['index']); } protected $relationTabs = [ 'multi_email' => [ 'icon' => 'contact_mail', 'permissions' => [ 'browse' => 'leads.browse', 'update' => 'leads.update', 'create' => 'leads.create' ], 'datatable' => [ 'datatable' => LeadEmailDatatable::class ], 'route' => [ 'linked' => 'leads.leademails.linked', 'create' => 'leademails.leademails.create', 'select' => 'leads.leademails.selection', 'bind_selected' => 'leads.leademails.link' ], 'create' => [ 'allow' => true, 'modal_title' => 'leademails::leademails.create_new', 'post_create_bind' => [ 'relationType' => 'oneToMany', 'relatedField' => 'lead_id', ] ], 'select' => [ 'allow' => false, 'modal_title' => 'leademails::leademails.module' ], ], 'lead_sent_emails' => [ 'icon' => 'mail', 'permissions' => [ 'browse' => 'leads.browse', 'update' => 'leads.update', 'create' => 'leads.create' ], 'datatable' => [ 'datatable' => LeadSentEmailsDatatable::class ], 'route' => [ 'linked' => 'leads.sentemails.linked', 'create' => 'sentemails.sentemails.create', 'select' => 'leads.sentemails.selection', 'bind_selected' => 'leads.sentemails.link' ], 'create' => [ 'allow' => true, 'modal_title' => 'sentemails::sentemails.create_new', 'label' => 'sentemails::sentemails.compose', 'post_create_bind' => [ 'relationType' => 'oneToMany', 'relatedField' => 'id', //TODO Fix this ], ], 'select' => [ 'allow' => false, 'modal_title' => 'sentemails::sentemails.module' ], ], 'sms' => [ 'icon' => 'sms', 'permissions' => [ 'browse' => 'sms.browse', 'update' => 'sms.update', 'create' => 'sms.create' ], 'datatable' => [ 'datatable' => LeadSmsDatatable::class ], 'route' => [ 'linked' => 'leads.sms.linked', 'create' => 'sms.sms.create', 'select' => 'leads.sms.selection', 'bind_selected' => 'leads.sms.link' ], 'create' => [ 'allow' => true, 'modal_title' => 'sms::sms.create_new', 'post_create_bind' => [ 'relationType' => 'oneToMany', 'relatedField' => 'lead_id', ] ], 'select' => [ 'allow' => false, 'modal_title' => 'sms::sms.module' ], ], 'events' => [ 'icon' => 'event', 'permissions' => [ 'browse' => 'event.browse', 'update' => 'event.update', 'create' => 'event.create' ], 'datatable' => [ 'datatable' => LeadEventsDatatable::class ], 'route' => [ 'linked' => 'leads.events.linked', 'create' => 'calendar.events.create', 'select' => 'leads.events.selection', 'bind_selected' => 'leads.events.link' ], 'create' => [ 'allow' => true, 'modal_title' => 'calendar::events.create_new', 'post_create_bind' => [ 'relationType' => 'oneToMany', 'relatedField' => 'lead_id', ] ], 'select' => [ 'allow' => false, 'modal_title' => 'calendar::events.module' ], ], 'calls' => [ 'icon' => 'phone', 'permissions' => [ 'browse' => 'calllogs.browse', 'update' => 'calllogs.update', 'create' => 'calllogs.create' ], 'datatable' => [ 'datatable' => LeadCallsDatatable::class ], 'route' => [ 'linked' => 'leads.calls.linked', 'create' => 'calllogs.calllogs.create', 'select' => 'leads.calls.selection', 'bind_selected' => 'leads.calls.link' ], 'create' => [ 'allow' => true, 'modal_title' => 'calllogs::calllogs.create_new', 'post_create_bind' => [ 'relationType' => 'oneToMany', 'relatedField' => 'lead_id', ] ], 'select' => [ 'allow' => false, 'modal_title' => 'calllogs::calllogs.module' ], ], 'tickets' => [ 'icon' => 'report_problem', 'permissions' => [ 'browse' => 'tickets.browse', 'update' => 'tickets.update', 'create' => 'tickets.create' ], 'datatable' => [ 'datatable' => LeadTicketsDatatable::class ], 'route' => [ 'linked' => 'leads.tickets.linked', 'create' => 'tickets.tickets.create', 'select' => 'leads.tickets.selection', 'bind_selected' => 'leads.tickets.link' ], 'create' => [ 'allow' => true, 'modal_title' => 'tickets::tickets.create_new', 'post_create_bind' => [ 'relationType' => 'oneToMany', 'relatedField' => 'lead_id', ] ], 'select' => [ 'allow' => false, 'modal_title' => 'tickets::tickets.module' ], ], 'call_requests' => [ 'icon' => 'phone', 'permissions' => [ 'browse' => 'contactrequests.browse', 'update' => 'contactrequests.update', 'create' => 'contactrequests.create' ], 'datatable' => [ 'datatable' => LeadCallRequestsDatatable::class ], 'route' => [ 'linked' => 'leads.contactrequests.linked', 'create' => 'contactrequests.contactrequests.create', 'select' => 'leads.contactrequests.selection', 'bind_selected' => 'leads.contactrequests.link' ], 'create' => [ 'allow' => true, 'modal_title' => 'contactrequests::contactrequests.create_new', 'post_create_bind' => [ 'relationType' => 'oneToMany', 'relatedField' => 'lead_id', ] ], 'select' => [ 'allow' => false, 'modal_title' => 'contactrequests::contactrequests.module' ], ], 'documents' => [ 'icon' => 'storage', 'permissions' => [ 'browse' => 'documents.browse', 'update' => 'documents.update', 'create' => 'documents.create' ], 'datatable' => [ 'datatable' => LeadDocumentsDatatable::class ], 'route' => [ 'linked' => 'leads.documents.linked', 'create' => 'documents.documents.create', 'select' => 'leads.documents.selection', 'bind_selected' => 'leads.documents.link' ], 'create' => [ 'allow' => true, 'modal_title' => 'documents::documents.create_new', 'post_create_bind' => [ 'relationType' => 'manyToMany', 'relatedField' => 'leads', ] ], 'select' => [ 'allow' => true, 'modal_title' => 'documents::documents.module' ], ], 'campaigns' => [ 'icon' => 'show_chart', 'permissions' => [ 'browse' => 'campaigns.browse', 'update' => 'campaigns.update', 'create' => 'campaigns.create' ], 'datatable' => [ 'datatable' => LeadCampaignsDatatable::class ], 'route' => [ 'linked' => 'leads.campaigns.linked', 'create' => 'campaigns.campaigns.create', 'select' => 'leads.campaigns.selection', 'bind_selected' => 'leads.campaigns.link' ], 'create' => [ 'allow' => false, 'modal_title' => 'campaigns::campaigns.create_new', 'post_create_bind' => [ 'relationType' => 'manyToMany', 'relatedField' => 'leads', ] ], 'select' => [ 'allow' => true, 'modal_title' => 'campaigns::campaigns.module' ], ], 'products' => [ 'icon' => 'pageview', 'permissions' => [ 'browse' => 'products.browse', 'update' => 'products.update', 'create' => 'products.create' ], 'datatable' => [ 'datatable' => LeadProductsDatatable::class ], 'route' => [ 'linked' => 'leads.products.linked', 'create' => 'products.products.create', 'select' => 'leads.products.selection', 'bind_selected' => 'leads.products.link' ], 'create' => [ 'allow' => false, 'modal_title' => 'products::products.create_new', 'post_create_bind' => [ 'relationType' => 'manyToMany', 'relatedField' => 'leads', ] ], 'select' => [ 'allow' => true, 'modal_title' => 'products::products.module' ], ], ]; protected $settingsPermission = 'leads.settings'; protected $showFields = [ 'lead_information' => [ 'first_name' => ['type' => 'text', 'col-class' => 'col-lg-4'], 'last_name' => ['type' => 'text', 'col-class' => 'col-lg-4'], 'full_name' => ['type' => 'text', 'col-class' => 'col-lg-4'], 'birth_date' => [ 'type' => 'date', 'col-class' => 'col-lg-4' ], 'lead_status_id' => [ 'type' => 'manyToOne', 'relation' => 'leadStatus', 'column' => 'name', 'col-class' => 'col-lg-4' ], 'language_id' => [ 'type' => 'manyToOne', 'relation' => 'language', 'column' => 'name', 'col-class' => 'col-lg-4' ], 'lead_source_id' => [ 'type' => 'manyToOne', 'relation' => 'leadSource', 'column' => 'name', 'col-class' => 'col-lg-4' ], 'lead_industry_id' => [ 'type' => 'manyToOne', 'relation' => 'leadIndustry', 'column' => 'name', 'col-class' => 'col-lg-4' ], 'lead_rating_id' => [ 'type' => 'manyToOne', 'relation' => 'leadRating', 'column' => 'name', 'col-class' => 'col-lg-4' ], 'annual_revenue' => ['type' => 'text', 'col-class' => 'col-lg-4'], 'referral_id' => [ 'type' => 'manyToOne', 'relation' => 'referral', 'column' => 'full_name', 'dont_translate' => true, 'col-class' => 'col-lg-4 col-md-4 col-sm-6 col-xs-6' ], 'capture_date' => [ 'type' => 'text', 'col-class' => 'col-lg-4' ], 'multiSelects' => ['type' => 'manyToMany', 'relation' => 'multiSelects', 'column' => 'name', 'col-class' => 'col-lg-8'], 'owned_by' => ['type' => 'assigned_to', 'col-class' => 'col-lg-4'], ], 'lead_contact_data' => [ 'phone' => ['type' => 'text'], 'mobile' => ['type' => 'text'], 'email' => ['type' => 'text'], 'secondary_email' => ['type' => 'text'], 'fax' => ['type' => 'text'], ], 'lead_company' => [ 'job_title' => ['type' => 'text'], 'website' => ['type' => 'text'], 'lead_company' => ['type' => 'text'], 'no_of_employees' => ['type' => 'text'], ], 'address_information' => [ 'addr_street' => ['type' => 'text','col-class' => 'col-lg-4 col-md-4 col-sm-6 col-xs-6'], 'addr_city' => ['type' => 'text', 'col-class' => 'col-lg-4 col-md-4 col-sm-6 col-xs-6'], 'addr_zip' => ['type' => 'text', 'col-class' => 'col-lg-4 col-md-4 col-sm-6 col-xs-6'], 'addr_state' => ['type' => 'text', 'col-class' => 'col-lg-4 col-md-4 col-sm-6 col-xs-6'], 'addr_country_id' => [ 'type' => 'manyToOne', 'relation' => 'addrCountry', 'column' => 'name', 'col-class' => 'col-lg-4 col-md-4 col-sm-6 col-xs-6' ], ], 'social' => [ 'skype' => ['type' => 'text', 'col-class' => 'col-lg-4'], 'facebook' => ['type' => 'text', 'col-class' => 'col-lg-4'], 'twitter' => ['type' => 'text', 'col-class' => 'col-lg-4'], ], 'description' => [ 'description' => ['type' => 'text', 'col-class' => 'col-lg-12 col-md-12 col-sm-12'], ], ]; protected $languageFile = 'leads::leads'; protected $routes = [ 'index' => 'leads.leads.index', 'create' => 'leads.leads.create', 'show' => 'leads.leads.show', 'edit' => 'leads.leads.edit', 'store' => 'leads.leads.store', 'destroy' => 'leads.leads.destroy', 'update' => 'leads.leads.update', 'import' => 'leads.leads.import', 'import_process' => 'leads.leads.import.process' ]; public function __construct() { parent::__construct(); $this->SettingsHelper = new SettingsHelper(); $this->leadFieldSettingsService = app(LeadFieldSettingsService::class); } /** * Map each Leads module "Settings" dropdown item to the corresponding * field-settings key so that renamed field labels (e.g. Status -> Test) * are reflected in the dropdown automatically. */ protected function moduleSettingsLinkFieldMap(): array { return [ 'settings.status' => 'lead_status_id', 'settings.source' => 'lead_source_id', 'settings.rating' => 'lead_rating_id', 'settings.industry' => 'lead_industry_id', 'settings.multiSelect' => 'multiSelects', ]; } /** * Effective field labels from the CRM field-settings service. */ protected function moduleSettingsLinkLabelMap(): array { return $this->leadFieldSettingsService->getFieldLabelMap(auth()->user()); } public function overrideShowFieldsForCurrentUser($defaultShowFields = []) { return $this->leadFieldSettingsService->getShowFields(auth()->user()); } protected function setupIndexTabButtons() { $this->indexTabButtons[] = array( 'href' => '#all', 'default' => true, 'attr' => [ 'class' => 'waves-effect waves-block', 'role' => 'tab', 'data-toggle'=>"tab", 'onClick' => "BAP_Datatable.headerFilterReset('dataTableBuilder')" ], 'label' => trans('core::core.header_all') ); $this->indexTabButtons[] = array( 'href' => '#assigned', 'attr' => [ 'class' => ' waves-effect waves-block', 'role' => 'tab', 'data-toggle'=>"tab", 'onClick' => "BAP_Datatable.headerFilterSet('assigned_to','dataTableBuilder',window.CURRENT_USER.name)" ], 'label' => trans('core::core.header_assigned') ); } public function beforeUpdate($request, &$entity = null, &$input = null) { if ($entity && $input && is_array($input) && Schema::hasColumn('leads', 'custom_fields')) { $existingCustomFields = is_array($entity->custom_fields ?? null) ? $entity->custom_fields : []; $incomingCustomFields = []; if (array_key_exists('custom_fields', $input) && is_array($input['custom_fields'])) { // Exclude any keys that now have real DB columns — those are submitted as // top-level fields and must NOT be packed into the JSON column. $fieldService = app(LeadFieldSettingsService::class); $provisionedKeys = $fieldService->getColumnBackedFieldKeys(auth()->user()); foreach ($input['custom_fields'] as $cfKey => $cfValue) { if (!in_array($cfKey, $provisionedKeys, true)) { $incomingCustomFields[$cfKey] = $cfValue; } } } $input['custom_fields'] = array_merge($existingCustomFields, $incomingCustomFields); } if ($input && !isset($input['owned_by']) && $entity) { if (!empty($entity->owned_by_type) && !empty($entity->owned_by_id)) { if (str_ends_with($entity->owned_by_type, 'User')) { $input['owned_by'] = 'user-' . $entity->owned_by_id; } elseif (str_ends_with($entity->owned_by_type, 'Group')) { $input['owned_by'] = 'group-' . $entity->owned_by_id; } } } } public function afterStore($request, &$entity = NULL) { if ($request->has('multiSelects')) { $entity->multiSelects()->sync($request->multiSelects); } // Set created_by and company_id $entity->created_by = \Auth::user()->id; $entity->company_id = \Auth::user()->company_id; if( $entity->owned_by_id == null ){ $entity->owned_by_id = \Auth::user()->id; $entity->owned_by_type = 'Modules\Platform\User\Entities\User'; } // Logging was disabled in setupAssignedTo() for the create flow so the // bookkeeping save above does not emit a second 'updated' activity entry. $entity->save(); $this->handleCustomFileUploads($request, $entity); $phones = collect([ $entity->phone, $entity->mobile, ])->filter() ->map(function ($p) { $p = trim($p); if (str_starts_with($p, '+20')) { $p = '0' . substr($p, 3); } $p = preg_replace('/\D+/', '', $p); return $p; }) ->unique() ->values(); if ($phones->isNotEmpty()) { // Update calls table (existing behavior) \Modules\Calls\Entities\Call::where('company_id', $entity->company_id) ->where(function ($q) use ($phones) { foreach ($phones as $phone) { $q->orWhereRaw("REPLACE(REPLACE(REPLACE(calls.phone_number, '+', ''), ' ', ''), '-', '') = ?", [$phone]); } }) ->update([ 'lead_id' => $entity->id ]); // Update call_logs table - phone_number is already normalized // Match Lead's normalized phone directly against call_logs.phone_number CallLog::whereIn('phone_number', $phones->toArray()) ->whereNull('lead_id') ->update([ 'lead_id' => $entity->id ]); } // Create flow is complete — restore automatic activity logging for any // subsequent saves on this entity. $entity->enableLogging(); } public function afterUpdate($request, &$entity = NULL) { $this->handleCustomFileUploads($request, $entity); } private function handleCustomFileUploads($request, $entity = null): void { if ($entity === null) { return; } $fileFields = $this->leadFieldSettingsService->getFileUploadFields(auth()->user()); if (empty($fileFields)) { return; } $customFields = is_array($entity->custom_fields) ? $entity->custom_fields : []; $customUpdated = false; $dirty = false; foreach ($fileFields as $field) { $storageKey = $field['storage_key'] ?? $field['key']; $usesColumn = !empty($field['uses_column']); $inputKey = $usesColumn ? $storageKey : 'custom_fields.' . $storageKey; $file = $request->file($inputKey); if (!$file) { continue; } $path = $file->store('leads/custom_fields', 'public'); if ($usesColumn) { $entity->{$storageKey} = $path; } else { $customFields[$storageKey] = $path; $customUpdated = true; } $dirty = true; } if ($dirty) { if ($customUpdated) { $entity->custom_fields = $customFields; } $entity->save(); } } public function report(Request $request){ $user = Auth::user(); $conds = $this->SettingsHelper->defaultConds(); $default_from = Carbon::now()->startOfMonth()->format('Y-m-d 00:00:00'); $default_to = Carbon::now()->format('Y-m-d 23:59:59'); if($request->has('daterange')){ $range = explode(' - ', $request->daterange); $filter_from = new Carbon($range[0]); $conds['from'] = $filter_from->format('Y-m-d 00:00:00'); $filter_to = new Carbon($range[1]); $conds['to'] = $filter_to->format('Y-m-d 23:59:59'); } else { $conds['from'] = $default_from; $conds['to'] = $default_to; } $data = \DB::table('leads') ->whereNull('leads.deleted_at') ->whereBetween('leads.created_at', [$conds['from'], $conds['to']]); // Add filters as needed... // Timeline: daily new leads $dailyLeads = (clone $data) ->select( \DB::raw('DATE(created_at) as date'), \DB::raw('COUNT(*) as count') ) ->groupBy('date') ->orderBy('date') ->get(); $period = new \DatePeriod( new \DateTime($conds['from']), new \DateInterval('P1D'), new \DateTime($conds['to']) ); $timeline = []; foreach ($period as $date) { $formattedDate = $date->format('Y-m-d'); $dayData = $dailyLeads->firstWhere('date', $formattedDate); $timeline[] = [ 'date' => $formattedDate, 'count' => $dayData->count ?? 0, ]; } // Pie chart: status distribution $statusCounts = (clone $data) ->leftJoin('leads_dict_status', 'leads.lead_status_id', '=', 'leads_dict_status.id') ->select('leads_dict_status.name as status_name', \DB::raw('COUNT(*) as count')) ->groupBy('leads_dict_status.name') ->pluck('count', 'status_name') ->toArray(); // Pie chart: source distribution $sourceCounts = (clone $data) ->leftJoin('leads_dict_source', 'leads.lead_source_id', '=', 'leads_dict_source.id') ->select('leads_dict_source.name as source_name', \DB::raw('COUNT(*) as count')) ->groupBy('leads_dict_source.name') ->pluck('count', 'source_name') ->toArray(); // Owner performance: number of leads per owner (ownedBy.name) $ownerCounts = (clone $data) ->leftJoin('users as owners', 'leads.owned_by_id', '=', 'owners.id') ->select('owners.name as owner_name', \DB::raw('COUNT(*) as count')) ->groupBy('owners.name') ->orderByDesc('count') ->pluck('count', 'owner_name') ->toArray(); $filter_area = view('reporting::widgets.filter_area'); $filter_area->with('conds', $conds); // // $filter_area->with('agents_options', $agents_options); // $filter_area->with('accounts_options', $accounts_options); // $filter_area->with('assignedToChoises_options', $assignedToChoises_options); $filter_area->with('user_ext', $request->user_ext); $filter_area->with('account', $request->account); $filter_area->with('assignedToChoise', $request->assignedToChoise); $filter_area->with('submit_route', route('leads.leads.report')); $view = view('leads::report'); $view->with('timeline', $timeline); $view->with('statusCounts', $statusCounts); $view->with('sourceCounts', $sourceCounts); $view->with('ownerCounts', $ownerCounts); $view->with('conds', $conds); $view->with('filter_area', $filter_area); // $view->with('agent_names', $agent_names); return $view; } /** * Prepare import - override to use dynamic field labels from CRM Settings. * * @param CsvImportRequest $request * @return \Illuminate\Contracts\View\Factory|\Illuminate\Http\RedirectResponse|\Illuminate\View\View */ protected function import(\Modules\Platform\Core\Http\Requests\CsvImportRequest $request) { try { if (\Auth::user()->hasPermissionTo($this->permissions['create'])) { $path = $request->file('csv_file'); $importConfig = ['delimiter' => $request->get('delimiter')]; $data = \Excel::toArray(new RecordImport($importConfig), $path); $data = $data[0]; if (count($data) > 0) { $view = view($this->views['import']); $csv_header_fields = []; foreach ($data[0] as $value) { $csv_header_fields[$value] = $value; } foreach ($data as &$row) { $row = array_combine($csv_header_fields, $row); } $view->with('csv_header', $csv_header_fields); $csvDataFile = \Modules\Platform\Core\Entities\CsvData::create([ 'csv_filename' => $request->file('csv_file')->getClientOriginalName(), 'csv_header' => 1, 'csv_data' => json_encode($data) ]); $csvData = array_slice($data, 0, 2); $moduleFields = \App::make($this->entityClass)->fillable; $moduleFields = \Modules\Platform\Core\Helper\CrudHelper::removeValues($moduleFields, [ 'id', 'company_id', 'full_name' ]); // Include custom fields created via CRM Settings in the // column/field mapping list so CSV data can be imported // directly into them. Labels come from $fieldLabelMap. $customFieldKeys = $this->leadFieldSettingsService->getCustomFieldKeys(\Auth::user()); $moduleFields = array_values(array_unique(array_merge($moduleFields, $customFieldKeys))); $view->with('csvDataFile', $csvDataFile); $view->with('module_fields', $moduleFields); $view->with('csv_data', $csvData); // Pass dynamic field labels from CRM Settings $labelMap = $this->leadFieldSettingsService->getFieldLabelMap(\Auth::user()); $view->with('fieldLabelMap', $labelMap); return $view; } else { flash(trans('core::core.empty_file'))->error(); return redirect()->back(); } } flash(trans('core::core.you_dont_have_access'))->error(); return redirect()->route($this->routes['index']); } catch (\Exception $exception) { flash(trans('core::core.invalid_file'))->error(); } } /** * Process import - override to use dynamic field definitions from CRM Settings. * * @param Request $request * @return \Illuminate\Http\RedirectResponse */ protected function importProcess(\Illuminate\Http\Request $request) { ini_set('max_execution_time', 0); $data = \Modules\Platform\Core\Entities\CsvData::find($request->get('file_id')); $repository = $this->getRepository(); if (!empty($data)) { $csvData = json_decode($data->csv_data, true); $imported = 0; $notImported = 0; $updated = 0; $errors = []; $requestFields = $request->get('fields'); $deduplicateColumn = $request->get('deduplicate_column'); $entityClass = \App::make($this->entityClass); $csvImporter = new CsvImporter(); $csvImporter->setEntity($entityClass); // Use dynamic show fields from CRM Settings $csvImporter->setFields($this->resolveShowFieldsForCurrentUser()); $csvImporter->setRelations(\Modules\Platform\Core\Helper\CrudHelper::relationships($entityClass)); $csvImporter->setRepo($repository); unset($csvData[0]); // remove header $csvImporter->preloadRelations($csvData, $requestFields); $batchRows = []; $upsertRows = []; $useTimestamps = method_exists($entityClass, 'usesTimestamps') && $entityClass->usesTimestamps(); $now = now(); foreach ($csvData as $rowKey => $row) { try { $fields = []; foreach ($requestFields as $key => $field) { $fields[$field] = $row[$key]; if ($field == 'owned_by_id') { $fields['owned_by_type'] = \Modules\Platform\User\Entities\User::class; } } $prepared = $csvImporter->prepareAttributes($fields); // Route custom field values (from CRM Settings) to their storage: // column-backed fields map to real columns and are left as-is, // while the remaining ones are packed into the custom_fields JSON. $prepared = $this->normalizeImportedCustomFields($prepared, \Auth::user()); if ($useTimestamps) { if (!array_key_exists('created_at', $prepared)) { $prepared['created_at'] = $now; } $prepared['updated_at'] = $now; } if ($deduplicateColumn != '' && array_key_exists($deduplicateColumn, $prepared)) { $upsertRows[] = $prepared; } else { $batchRows[] = $prepared; if (count($batchRows) >= $this->importChunkSize) { \DB::table($entityClass->getTable())->insert($batchRows); $imported += count($batchRows); $batchRows = []; } } } catch (\Exception $exception) { $notImported++; \Log::error($exception); } } if (count($batchRows) > 0) { \DB::table($entityClass->getTable())->insert($batchRows); $imported += count($batchRows); } if ($deduplicateColumn != '' && count($upsertRows) > 0) { $dedupeValues = array_values(array_filter(array_column($upsertRows, $deduplicateColumn), function ($value) { return $value !== null && $value !== ''; })); $existingCount = 0; if (count($dedupeValues) > 0) { $existingCount = \App::make($this->entityClass)->whereIn($deduplicateColumn, array_unique($dedupeValues))->count(); } $updated += $existingCount; $imported += count($upsertRows) - $existingCount; $updateColumns = []; if (!empty($upsertRows)) { $updateColumns = array_diff(array_keys($upsertRows[0]), [$deduplicateColumn, 'id']); if ($useTimestamps && in_array('updated_at', array_keys($upsertRows[0]))) { $updateColumns = array_unique(array_merge($updateColumns, ['updated_at'])); } } foreach (array_chunk($upsertRows, $this->importChunkSize) as $chunk) { \DB::table($entityClass->getTable())->upsert($chunk, [$deduplicateColumn], $updateColumns); } } flash(trans('core::core.import_finished', ['imported' => $imported, 'not_imported' => $notImported, 'updated' => $updated]))->info(); return redirect()->route($this->routes['index']); } else { flash(trans('core::core.cannot_process_import'))->error(); return redirect()->route($this->routes['index']); } } /** * Move custom field values (created via CRM Settings) that are NOT backed by a * real provisioned column into the `custom_fields` JSON column so batch inserts * persist them correctly. Column-backed custom fields are untouched (they already * map to real DB columns). * * @param array $attributes * @param \Modules\Platform\User\Entities\User $user * @return array */ private function normalizeImportedCustomFields(array $attributes, $user): array { $jsonBackedFields = $this->leadFieldSettingsService->getJsonBackedCustomFields($user); if (empty($jsonBackedFields)) { return $attributes; } $customFieldValues = []; foreach ($jsonBackedFields as $key => $definition) { if (!array_key_exists($key, $attributes)) { continue; } $value = $attributes[$key]; // Multi-select custom fields are stored as arrays; convert a // comma-separated CSV value into one. if (($definition['type'] ?? null) === 'manytomany' && is_string($value) && $value !== '') { $value = array_values(array_filter(array_map('trim', explode(',', $value)))); } if ($value !== null && $value !== '') { $customFieldValues[$key] = $value; } unset($attributes[$key]); } if (!empty($customFieldValues)) { $attributes['custom_fields'] = json_encode($customFieldValues); } return $attributes; } }