'Text', 'textarea' => 'Textarea', 'email' => 'Email', 'tel' => 'Phone', 'number' => 'Number', 'url' => 'URL', 'date' => 'Date', 'datetime' => 'Date & Time', 'select' => 'Dropdown', 'checkbox' => 'Checkbox', 'tags' => 'Tags', 'assigned_to' => 'Assigned To', 'manytoone' => 'Lookup', 'file' => 'File', ]; private const PANEL_ORDER = [ 'information' => 10, 'additional_information' => 20, 'address_information' => 30, 'notes' => 40, 'contact_data' => 50, ]; private $hasSettingsTable = null; private $hasCustomFieldsColumn = null; public function getSettingsEditorData(User $user) { $fields = $this->getEffectiveFields($user); $grouped = []; foreach (self::EDITABLE_PANELS as $panel) { $grouped[$panel] = []; } foreach ($fields as $field) { $field['type_options'] = $this->getTypeOptionsForAllowedTypes($field['allowed_types']); $field['is_type_locked'] = !$field['can_change_type']; $field['is_required_locked'] = !$field['can_change_required']; $field['is_visibility_locked'] = !$field['can_change_visibility']; $field['is_max_length_locked'] = !$field['can_change_max_length']; $field['is_order_locked'] = !Arr::get($field, 'can_change_order', true); $panel = $field['panel']; if (!isset($grouped[$panel])) { $grouped[$panel] = []; } $grouped[$panel][] = $field; } foreach ($grouped as $panel => &$panelFields) { usort($panelFields, function ($a, $b) { return ($a['order'] ?? 0) <=> ($b['order'] ?? 0); }); } unset($panelFields); return [ 'groupedFields' => $grouped, 'allTypeOptions' => self::TYPE_OPTIONS, 'panelOptions' => $this->getPanelLabels($user), ]; } public function getSelectableFieldOptions(User $user): array { $options = []; foreach ($this->getEffectiveFields($user) as $field) { if ($this->isSelectableField($field)) { $options[$field['key']] = $field['label']; } } return $options; } public function getVisibleCustomFieldsForIndex(User $user) { $fields = []; foreach ($this->getEffectiveFields($user) as $field) { if (!$field['is_custom'] || !$field['visible']) { continue; } $fields[] = $field; } return $fields; } /** * Return all visible fields for the API (same contract as LeadFieldSettingsService). */ public function getVisibleFieldsForApi(User $user): array { $result = []; foreach ($this->getEffectiveFields($user) as $field) { if (!$field['visible']) { continue; } $result[] = $field; } return $result; } /** * Return all custom field keys (visible or not). */ public function getCustomFieldKeys(User $user): array { $keys = []; foreach ($this->getEffectiveFields($user) as $field) { if ($field['is_custom']) { $keys[] = $field['key']; } } return $keys; } /** * Return field definitions that expect file uploads (custom fields only). */ public function getFileUploadFields(User $user): array { $fields = []; foreach ($this->getEffectiveFields($user) as $field) { if (($field['type'] ?? null) !== 'file') { continue; } if (!empty($field['special'])) { continue; } $fields[] = $field; } return $fields; } /** * Return all column keys ever auto-provisioned via CRM settings. */ public function getProvisionedColumnKeys(User $user): array { $settings = $this->getStoredSettings($user); $keys = Arr::get($settings, 'provisioned_columns', []); return is_array($keys) ? array_values($keys) : []; } /** * Return the flagged API index field keys (max 4). */ public function getApiIndexFieldKeys(User $user): array { $settings = $this->getStoredSettings($user); $keys = Arr::get($settings, 'api_index_fields', []); if (!is_array($keys)) { $keys = []; } return array_slice(array_values(array_filter($keys)), 0, 4); } /** * Return full field definitions for the flagged API index fields. */ public function getApiIndexFields(User $user): array { $flaggedKeys = $this->getApiIndexFieldKeys($user); if (empty($flaggedKeys)) { return []; } $allFields = $this->getEffectiveFields($user); $result = []; foreach ($flaggedKeys as $key) { foreach ($allFields as $field) { if ($field['key'] === $key && $field['visible']) { $result[] = [ 'key' => $field['key'], 'label' => $field['label'], 'type' => $field['type'], 'is_custom' => $field['is_custom'], 'special' => $field['special'], 'uses_column' => $field['uses_column'] ?? false, ]; break; } } } return $result; } public function getVisibleFieldKeys(User $user) { $keys = []; foreach ($this->getEffectiveFields($user) as $field) { if ($field['visible']) { $keys[] = $field['key']; } } return $keys; } public function getFieldOrderMap(User $user) { $map = []; $panelOrder = self::PANEL_ORDER; foreach ($this->getEffectiveFields($user) as $field) { $panelBase = ($panelOrder[$field['panel']] ?? 99) * 1000; $map[$field['key']] = $panelBase + $field['order']; } return $map; } /** Map of key => current label (including any user overrides). */ public function getFieldLabelMap(User $user) { $map = []; foreach ($this->getEffectiveFields($user) as $field) { $map[$field['key']] = $field['label']; } return $map; } public function getShowFields(User $user) { $groupedPanels = []; foreach ($this->getEffectiveFields($user) as $field) { if (!$field['visible']) { continue; } $panel = $field['panel']; $fieldName = $field['form_name']; $options = [ 'col-class' => $field['col_class'], 'label' => $field['label'], ]; if (!empty($field['hide_in_form'])) { $options['hide_in_form'] = true; } if (!empty($field['hide_in_show'])) { $options['hide_in_show'] = true; } if (!empty($field['hide_label'])) { $options['hide_label'] = true; } if ($field['is_custom']) { $options['type'] = 'custom'; $options['display_type'] = $this->mapShowType($field['type']); $options['custom_field_key'] = $field['storage_key']; } elseif ($field['special'] !== null) { $options = array_merge($options, $this->specialShowOptions($field)); } else { $options['type'] = $this->mapShowType($field['type']); } if (!isset($groupedPanels[$panel])) { $groupedPanels[$panel] = []; } $groupedPanels[$panel][$fieldName] = $options; } $sorted = []; foreach (self::PANEL_ORDER as $panel => $order) { if (!empty($groupedPanels[$panel])) { $sorted[$panel] = $groupedPanels[$panel]; } } foreach ($groupedPanels as $panel => $fields) { if (!isset($sorted[$panel])) { $sorted[$panel] = $fields; } } return $sorted; } public function buildFormFields(Form $form, User $user, $model = null) { $customValues = []; if ($model instanceof Contact && is_array($model->custom_fields)) { $customValues = $model->custom_fields; } foreach ($this->getEffectiveFields($user) as $field) { if (!$field['visible']) { continue; } if (!empty($field['hide_in_form'])) { continue; } if ($field['special'] !== null) { $this->addSpecialField($form, $field, $user, $model); continue; } $value = null; if ($field['is_custom']) { if (!empty($field['uses_column']) && $model !== null) { $value = $model->{$field['storage_key']} ?? null; } else { $value = Arr::get($customValues, $field['storage_key']); } } $this->addSimpleField($form, $field, $value); } } public function getValidationRules(User $user) { $rules = []; $hasVisibleCustomFields = false; foreach ($this->getEffectiveFields($user) as $field) { if (!$field['visible']) { continue; } if ($field['is_custom'] && empty($field['uses_column'])) { $hasVisibleCustomFields = true; } if ($field['special'] !== null) { $rules = array_merge($rules, $this->specialValidationRules($field)); continue; } $rules[$field['validation_key']] = $this->simpleValidationRules($field); } if ($hasVisibleCustomFields && !isset($rules['custom_fields'])) { $rules['custom_fields'] = ['nullable', 'array']; } return $rules; } public function saveSettings(User $user, array $input) { if (!$this->hasSettingsTable()) { return false; } $defaults = $this->defaultFieldDefinitions($user); $builtInInput = Arr::get($input, 'built_in', []); $customInput = Arr::get($input, 'custom', []); if (!is_array($builtInInput)) { $builtInInput = []; } if (!is_array($customInput)) { $customInput = []; } $builtIn = []; foreach ($defaults as $key => $field) { $payload = Arr::get($builtInInput, $key, []); if (!is_array($payload)) { $payload = []; } $label = trim((string) Arr::get($payload, 'label', $field['label'])); if ($label === '') { $label = $field['label']; } $visible = $field['can_change_visibility'] ? $this->normalizeBool(Arr::get($payload, 'visible', $field['visible'])) : $field['visible']; $required = $field['can_change_required'] ? $this->normalizeBool(Arr::get($payload, 'required', $field['required'])) : $field['required']; if (!$visible) { $required = false; } $type = $field['can_change_type'] ? $this->normalizeType(Arr::get($payload, 'type', $field['type']), $field['allowed_types'], $field['type']) : $field['type']; $maxLength = $field['can_change_max_length'] ? $this->normalizeMaxLength(Arr::get($payload, 'max_length', $field['max_length']), $type) : $field['max_length']; $order = $field['can_change_order'] ? $this->normalizeOrder(Arr::get($payload, 'order', $field['order']), $field['order']) : $field['order']; $panel = $this->normalizePanel(Arr::get($payload, 'panel', $field['panel'])); $builtIn[$key] = [ 'label' => $label, 'visible' => $visible, 'required' => $required, 'type' => $type, 'max_length' => $maxLength, 'order' => $order, 'panel' => $panel, ]; } $custom = []; $reservedKeys = array_keys($defaults); $used = []; $customOrderFallback = 2000; if ($this->supportsCustomFields()) { foreach ($customInput as $row) { if (!is_array($row)) { continue; } $rawKey = Arr::get($row, 'key', Arr::get($row, 'name', '')); $key = $this->sanitizeCustomKey($rawKey); if ($key === '' || in_array($key, $reservedKeys, true) || isset($used[$key])) { continue; } $used[$key] = true; $label = trim((string) Arr::get($row, 'label', '')); if ($label === '') { $label = Str::title(str_replace('_', ' ', $key)); } $visible = $this->normalizeBool(Arr::get($row, 'visible', true)); $required = $visible ? $this->normalizeBool(Arr::get($row, 'required', false)) : false; $type = $this->normalizeType(Arr::get($row, 'type', 'text'), array_keys(self::TYPE_OPTIONS), 'text'); $maxLength = $this->normalizeMaxLength(Arr::get($row, 'max_length', null), $type); $options = $this->normalizeOptions(Arr::get($row, 'options', [])); if (!$this->supportsOptionsForType($type)) { $options = []; } $panel = $this->normalizePanel(Arr::get($row, 'panel', self::DEFAULT_CUSTOM_PANEL)); $order = $this->normalizeOrder(Arr::get($row, 'order', null), $customOrderFallback++); $custom[] = [ 'key' => $key, 'label' => $label, 'visible' => $visible, 'required' => $required, 'type' => $type, 'max_length' => $maxLength, 'options' => $options, 'panel' => $panel, 'order' => $order, '_depends_on_raw' => Arr::get($row, 'depends_on'), '_depends_map_raw' => Arr::get($row, 'depends_map'), ]; } usort($custom, function ($a, $b) { $sort = ($a['order'] ?? 0) <=> ($b['order'] ?? 0); if ($sort !== 0) { return $sort; } return strcmp((string) ($a['key'] ?? ''), (string) ($b['key'] ?? '')); }); $custom = array_values($custom); } $selectableKeys = $this->collectSelectableKeys($defaults, $builtIn, $custom); foreach ($builtIn as $key => &$entry) { $payload = Arr::get($builtInInput, $key, []); $dependsOn = $this->normalizeDependencyParent( Arr::get($payload, 'depends_on'), $selectableKeys, $key ); $dependsMap = $this->normalizeDependencyMap(Arr::get($payload, 'depends_map')); $special = $defaults[$key]['special'] ?? null; if (!$this->isSelectableDefinition($entry['type'] ?? null, $special)) { $dependsOn = null; $dependsMap = []; } $entry['depends_on'] = $dependsOn; $entry['depends_map'] = $dependsMap; } unset($entry); foreach ($custom as &$entry) { $dependsOn = $this->normalizeDependencyParent( $entry['_depends_on_raw'] ?? null, $selectableKeys, $entry['key'] ); $dependsMap = $this->normalizeDependencyMap($entry['_depends_map_raw'] ?? null); if (!$this->isSelectableDefinition($entry['type'] ?? null, $entry['special'] ?? null)) { $dependsOn = null; $dependsMap = []; } $entry['depends_on'] = $dependsOn; $entry['depends_map'] = $dependsMap; unset($entry['_depends_on_raw'], $entry['_depends_map_raw']); } unset($entry); $record = ContactUserFieldSetting::query() ->where('company_id', $user->company_id) ->whereNull('user_id') ->first(); if (!$record) { $record = new ContactUserFieldSetting(); $record->company_id = $user->company_id; } // Auto-provision real DB columns for custom fields and track them. $previousSettings = is_array($record->settings ?? null) ? $record->settings : []; $provisionedColumns = array_values(Arr::get($previousSettings, 'provisioned_columns', [])); if (!is_array($provisionedColumns)) { $provisionedColumns = []; } // Retroactively register keys from previous settings that already have a column foreach (array_column(Arr::get($previousSettings, 'custom', []), 'key') as $prevKey) { if ($prevKey !== '' && Schema::hasColumn('contacts', $prevKey) && !in_array($prevKey, $provisionedColumns, true)) { $provisionedColumns[] = $prevKey; } } // Broader retroactive scan: any contacts column that isn't a known built-in $builtInKeys = array_keys($this->defaultFieldDefinitions($user)); $alwaysBuiltIn = [ 'id', 'created_at', 'updated_at', 'deleted_at', 'company_id', 'owned_by_type', 'owned_by_id', 'created_by', 'custom_fields', 'full_name', 'country', 'gravatar', 'profile_image', 'use_profile_image', 'tags', 'force_assigned', ]; $knownColumns = array_merge($builtInKeys, $alwaysBuiltIn); foreach (Schema::getColumnListing('contacts') as $col) { if (!in_array($col, $knownColumns, true) && !in_array($col, $provisionedColumns, true)) { $provisionedColumns[] = $col; } } // Create new columns for newly-added custom fields foreach ($custom as $fieldDef) { $cfKey = $fieldDef['key']; if (!Schema::hasColumn('contacts', $cfKey)) { Schema::table('contacts', function (Blueprint $table) use ($cfKey) { $table->string($cfKey)->nullable(); }); // Migrate any existing JSON value to the new real column DB::table('contacts') ->whereNotNull('custom_fields') ->lazyById() ->each(function ($row) use ($cfKey) { $data = is_string($row->custom_fields) ? json_decode($row->custom_fields, true) : null; if (is_array($data) && array_key_exists($cfKey, $data) && $data[$cfKey] !== null) { DB::table('contacts')->where('id', $row->id)->update([$cfKey => $data[$cfKey]]); } }); } if (!in_array($cfKey, $provisionedColumns, true)) { $provisionedColumns[] = $cfKey; } } $apiIndexFields = Arr::get($input, 'api_index_fields', []); if (!is_array($apiIndexFields)) { $apiIndexFields = []; } $apiIndexFields = array_values(array_filter($apiIndexFields, 'is_string')); // Only allow API index flags on visible fields $visibleKeys = []; foreach ($builtIn as $key => $bi) { if (!empty($bi['visible'])) { $visibleKeys[] = $key; } } foreach ($custom as $cf) { if (!empty($cf['visible'])) { $visibleKeys[] = $cf['key']; } } $apiIndexFields = array_slice(array_values(array_intersect($apiIndexFields, $visibleKeys)), 0, 4); $panelLabels = $this->normalizePanelLabels(Arr::get($input, 'panel_labels', [])); $record->settings = [ 'built_in' => $builtIn, 'custom' => $custom, 'api_index_fields' => $apiIndexFields, 'provisioned_columns' => array_values(array_unique($provisionedColumns)), 'panel_labels' => $panelLabels, ]; $record->save(); $this->hasCustomFieldsColumn = null; return true; } public function resetSettings(User $user): bool { if (!$this->hasSettingsTable()) { return false; } ContactUserFieldSetting::query() ->where('company_id', $user->company_id) ->whereNull('user_id') ->delete(); $this->hasCustomFieldsColumn = null; return true; } private function getEffectiveFields(User $user) { $definitions = $this->defaultFieldDefinitions($user); $settings = $this->getStoredSettings($user); foreach ($definitions as $key => &$field) { $overrides = Arr::get($settings, 'built_in.' . $key, []); if (!is_array($overrides)) { $overrides = []; } $label = trim((string) Arr::get($overrides, 'label', $field['label'])); if ($label !== '') { $field['label'] = $label; } if ($field['can_change_visibility']) { $field['visible'] = $this->normalizeBool(Arr::get($overrides, 'visible', $field['visible'])); } if ($field['can_change_required']) { $field['required'] = $this->normalizeBool(Arr::get($overrides, 'required', $field['required'])); } if ($field['can_change_type']) { $field['type'] = $this->normalizeType( Arr::get($overrides, 'type', $field['type']), $field['allowed_types'], $field['type'] ); } if ($field['can_change_max_length']) { $field['max_length'] = $this->normalizeMaxLength( Arr::get($overrides, 'max_length', $field['max_length']), $field['type'] ); } if ($field['can_change_order']) { $field['order'] = $this->normalizeOrder( Arr::get($overrides, 'order', $field['order']), $field['order'] ); } if (Arr::has($overrides, 'panel')) { $field['panel'] = $this->normalizePanel($overrides['panel']); } $field['depends_on'] = Arr::get($overrides, 'depends_on'); $field['depends_map'] = $this->normalizeDependencyMap(Arr::get($overrides, 'depends_map', [])); if (!$field['visible']) { $field['required'] = false; } } unset($field); $custom = $this->supportsCustomFields() ? $this->customFieldDefinitions($settings, array_keys($definitions)) : []; $all = array_merge($definitions, $custom); $all = $this->normalizeFieldDependencies($all); $panelOrder = self::PANEL_ORDER; uasort($all, function ($a, $b) use ($panelOrder) { $panelA = $panelOrder[$a['panel']] ?? 99; $panelB = $panelOrder[$b['panel']] ?? 99; if ($panelA !== $panelB) { return $panelA <=> $panelB; } return ($a['order'] ?? 0) <=> ($b['order'] ?? 0); }); return $all; } private function defaultFieldDefinitions(User $user) { $fields = [ 'full_name' => $this->makeBuiltInField('full_name', [ 'label' => trans('contacts::contacts.form.full_name'), 'panel' => 'information', 'col_class' => 'col-lg-6 col-md-6 col-sm-6 col-xs-6', 'max_length' => 255, 'allowed_types' => ['text'], 'can_change_type' => false, 'can_change_required' => false, 'hide_in_form' => true, 'order' => 10, ]), 'first_name' => $this->makeBuiltInField('first_name', [ 'label' => trans('contacts::contacts.form.first_name'), 'panel' => 'information', 'col_class' => 'col-lg-6 col-md-6 col-sm-6 col-xs-6', 'required' => true, 'max_length' => 255, 'allowed_types' => ['text'], 'can_change_type' => false, 'can_change_required' => false, 'can_change_visibility' => false, 'can_change_max_length' => false, 'hide_in_show' => true, 'order' => 20, ]), 'last_name' => $this->makeBuiltInField('last_name', [ 'label' => trans('contacts::contacts.form.last_name'), 'panel' => 'information', 'col_class' => 'col-lg-6 col-md-6 col-sm-6 col-xs-6', 'required' => true, 'max_length' => 255, 'allowed_types' => ['text'], 'can_change_type' => false, 'can_change_required' => false, 'hide_in_show' => true, 'order' => 30, ]), 'use_profile_image' => $this->makeBuiltInField('use_profile_image', [ 'label' => trans('contacts::contacts.form.use_profile_image'), 'panel' => 'information', 'col_class' => 'col-lg-3 col-md-3 col-sm-6 col-xs-6', 'type' => 'checkbox', 'allowed_types' => ['checkbox'], 'can_change_type' => false, 'can_change_max_length' => false, 'hide_in_show' => true, 'order' => 40, ]), 'force_assigned' => $this->makeBuiltInField('force_assigned', [ 'label' => 'Force Assigned', 'panel' => 'information', 'col_class' => 'col-lg-3 col-md-3 col-sm-6 col-xs-6', 'type' => 'checkbox', 'allowed_types' => ['checkbox'], 'can_change_type' => false, 'can_change_max_length' => false, 'hide_in_show' => true, 'order' => 50, ]), 'profile_image' => $this->makeBuiltInField('profile_image', [ 'label' => trans('contacts::contacts.form.profile_image'), 'panel' => 'information', 'col_class' => 'col-lg-6 col-md-6 col-sm-6 col-xs-6', 'special' => 'profile_image', 'type' => 'file', 'allowed_types' => ['file'], 'can_change_type' => false, 'can_change_max_length' => false, 'hide_label' => true, 'order' => 60, ]), 'phone' => $this->makeBuiltInField('phone', [ 'label' => trans('contacts::contacts.form.phone'), 'panel' => 'information', 'col_class' => 'col-lg-6 col-md-6 col-sm-6 col-xs-6', 'type' => 'tel', 'allowed_types' => ['text', 'tel'], 'max_length' => 50, 'order' => 70, ]), 'mobile' => $this->makeBuiltInField('mobile', [ 'label' => trans('contacts::contacts.form.mobile'), 'panel' => 'information', 'col_class' => 'col-lg-6 col-md-6 col-sm-6 col-xs-6', 'type' => 'tel', 'allowed_types' => ['text', 'tel'], 'max_length' => 50, 'order' => 90, ]), 'email' => $this->makeBuiltInField('email', [ 'label' => trans('contacts::contacts.form.email'), 'panel' => 'information', 'col_class' => 'col-lg-6 col-md-6 col-sm-6 col-xs-6', 'type' => 'email', 'allowed_types' => ['text', 'email'], 'max_length' => 255, 'order' => 110, ]), 'fax' => $this->makeBuiltInField('fax', [ 'label' => trans('contacts::contacts.form.fax'), 'panel' => 'additional_information', 'col_class' => 'col-lg-6 col-md-6 col-sm-6 col-xs-6', 'type' => 'tel', 'allowed_types' => ['text', 'tel'], 'max_length' => 50, 'order' => 10, ]), 'birth_date' => $this->makeBuiltInField('birth_date', [ 'label' => trans('contacts::contacts.form.birth_date'), 'panel' => 'additional_information', 'col_class' => 'col-lg-6 col-md-6 col-sm-6 col-xs-6', 'type' => 'date', 'allowed_types' => ['date', 'datetime'], 'can_change_max_length' => false, 'order' => 90, ]), 'job_title' => $this->makeBuiltInField('job_title', [ 'label' => trans('contacts::contacts.form.job_title'), 'panel' => 'additional_information', 'col_class' => 'col-lg-6 col-md-6 col-sm-6 col-xs-6', 'max_length' => 255, 'order' => 40, ]), 'department' => $this->makeBuiltInField('department', [ 'label' => trans('contacts::contacts.form.department'), 'panel' => 'additional_information', 'col_class' => 'col-lg-6 col-md-6 col-sm-6 col-xs-6', 'max_length' => 255, 'order' => 60, ]), 'assistant_name' => $this->makeBuiltInField('assistant_name', [ 'label' => trans('contacts::contacts.form.assistant_name'), 'panel' => 'additional_information', 'col_class' => 'col-lg-6 col-md-6 col-sm-6 col-xs-6', 'max_length' => 255, 'order' => 50, ]), 'assistant_phone' => $this->makeBuiltInField('assistant_phone', [ 'label' => trans('contacts::contacts.form.assistant_phone'), 'panel' => 'additional_information', 'col_class' => 'col-lg-6 col-md-6 col-sm-6 col-xs-6', 'type' => 'tel', 'allowed_types' => ['text', 'tel'], 'max_length' => 50, 'order' => 70, ]), 'contact_status_id' => $this->makeBuiltInField('contact_status_id', [ 'label' => trans('contacts::contacts.form.contact_status_id'), 'panel' => 'information', 'col_class' => 'col-lg-6 col-md-6 col-sm-6 col-xs-6', 'special' => 'contact_status_select', 'type' => 'manytoone', 'allowed_types' => ['manytoone'], 'can_change_type' => false, 'can_change_max_length' => false, 'order' => 100, ]), 'contact_source_id' => $this->makeBuiltInField('contact_source_id', [ 'label' => trans('contacts::contacts.form.contact_source_id'), 'panel' => 'additional_information', 'col_class' => 'col-lg-6 col-md-6 col-sm-6 col-xs-6', 'special' => 'contact_source_select', 'type' => 'manytoone', 'allowed_types' => ['manytoone'], 'can_change_type' => false, 'can_change_max_length' => false, 'order' => 20, ]), 'tags' => $this->makeBuiltInField('tags', [ 'label' => trans('contacts::contacts.form.tags'), 'panel' => 'additional_information', 'col_class' => 'col-lg-12 col-md-12 col-sm-12 col-xs-12', 'special' => 'tags', 'type' => 'tags', 'allowed_types' => ['tags'], 'can_change_type' => false, 'can_change_max_length' => false, 'order' => 30, ]), 'language_id' => $this->makeBuiltInField('language_id', [ 'label' => trans('contacts::contacts.form.language_id'), 'panel' => 'additional_information', 'col_class' => 'col-lg-6 col-md-6 col-sm-6 col-xs-6', 'special' => 'language_select', 'type' => 'manytoone', 'allowed_types' => ['manytoone'], 'can_change_type' => false, 'can_change_max_length' => false, 'order' => 100, ]), 'account_id' => $this->makeBuiltInField('account_id', [ 'label' => trans('core::core.form.account_id'), 'panel' => 'information', 'col_class' => 'col-lg-6 col-md-6 col-sm-6 col-xs-6', 'special' => 'account_many_to_one', 'type' => 'manytoone', 'allowed_types' => ['manytoone'], 'can_change_type' => false, 'can_change_max_length' => false, 'order' => 120, ]), 'referral_id' => $this->makeBuiltInField('referral_id', [ 'label' => trans('contacts::contacts.form.referral_id'), 'panel' => 'additional_information', 'col_class' => 'col-lg-6 col-md-6 col-sm-6 col-xs-6', 'special' => 'referral_many_to_one', 'type' => 'manytoone', 'allowed_types' => ['manytoone'], 'can_change_type' => false, 'can_change_max_length' => false, 'order' => 80, ]), 'country_id' => $this->makeBuiltInField('country_id', [ 'label' => trans('contacts::contacts.form.country_id'), 'panel' => 'address_information', 'col_class' => 'col-lg-4 col-md-4 col-sm-6 col-xs-6', 'special' => 'country_select', 'type' => 'manytoone', 'allowed_types' => ['manytoone'], 'can_change_type' => false, 'can_change_max_length' => false, 'order' => 50, ]), 'street' => $this->makeBuiltInField('street', [ 'label' => trans('contacts::contacts.form.street'), 'panel' => 'address_information', 'col_class' => 'col-lg-4 col-md-4 col-sm-6 col-xs-6', 'max_length' => 255, 'order' => 10, ]), 'city' => $this->makeBuiltInField('city', [ 'label' => trans('contacts::contacts.form.city'), 'panel' => 'address_information', 'col_class' => 'col-lg-4 col-md-4 col-sm-6 col-xs-6', 'max_length' => 255, 'order' => 20, ]), 'state' => $this->makeBuiltInField('state', [ 'label' => trans('contacts::contacts.form.state'), 'panel' => 'address_information', 'col_class' => 'col-lg-4 col-md-4 col-sm-6 col-xs-6', 'max_length' => 255, 'order' => 40, ]), 'zip_code' => $this->makeBuiltInField('zip_code', [ 'label' => trans('contacts::contacts.form.zip_code'), 'panel' => 'address_information', 'col_class' => 'col-lg-4 col-md-4 col-sm-6 col-xs-6', 'max_length' => 20, 'order' => 30, ]), 'notes' => $this->makeBuiltInField('notes', [ 'label' => trans('contacts::contacts.form.notes'), 'panel' => 'notes', 'col_class' => 'col-lg-12 col-md-12 col-sm-12', 'type' => 'textarea', 'allowed_types' => ['textarea', 'text'], 'max_length' => 5000, 'order' => 10, ]), ]; if ($user->hasPermissionTo('contacts.update') || $user->hasPermissionTo('contacts.create')) { $fields['owned_by'] = $this->makeBuiltInField('owned_by', [ 'label' => trans('core::core.form.assigned_to'), 'panel' => 'information', 'col_class' => 'col-lg-6 col-md-6 col-sm-6 col-xs-6', 'special' => 'owner_select', 'type' => 'assigned_to', 'allowed_types' => ['assigned_to'], 'can_change_type' => false, 'can_change_max_length' => false, 'order' => 80, ]); } return $fields; } private function customFieldDefinitions(array $settings, array $reservedKeys) { $definitions = []; $rows = Arr::get($settings, 'custom', []); $position = 2000; $used = []; if (!is_array($rows)) { return $definitions; } foreach ($rows as $row) { if (!is_array($row)) { continue; } $key = $this->sanitizeCustomKey(Arr::get($row, 'key', '')); if ($key === '' || in_array($key, $reservedKeys, true) || isset($used[$key])) { continue; } $used[$key] = true; $type = $this->normalizeType(Arr::get($row, 'type', 'text'), array_keys(self::TYPE_OPTIONS), 'text'); $label = trim((string) Arr::get($row, 'label', '')); if ($label === '') { $label = Str::title(str_replace('_', ' ', $key)); } $visible = $this->normalizeBool(Arr::get($row, 'visible', true)); $required = $visible ? $this->normalizeBool(Arr::get($row, 'required', false)) : false; $maxLength = $this->normalizeMaxLength(Arr::get($row, 'max_length', null), $type); $options = $this->normalizeOptions(Arr::get($row, 'options', [])); if (!$this->supportsOptionsForType($type)) { $options = []; } $panel = $this->normalizePanel(Arr::get($row, 'panel', self::DEFAULT_CUSTOM_PANEL)); $order = $this->normalizeOrder(Arr::get($row, 'order', null), $position++); $definitions['custom:' . $key] = [ 'id' => 'custom:' . $key, 'key' => $key, 'storage_key' => $key, 'form_name' => Schema::hasColumn('contacts', $key) ? $key : 'custom_fields[' . $key . ']', 'validation_key' => Schema::hasColumn('contacts', $key) ? $key : 'custom_fields.' . $key, 'uses_column' => Schema::hasColumn('contacts', $key), 'panel' => $panel, 'label' => $label, 'type' => $type, 'allowed_types' => array_keys(self::TYPE_OPTIONS), 'required' => $required, 'visible' => $visible, 'max_length' => $maxLength, 'options' => $options, 'depends_on' => Arr::get($row, 'depends_on'), 'depends_map' => $this->normalizeDependencyMap(Arr::get($row, 'depends_map', [])), 'col_class' => 'col-lg-6 col-md-6 col-sm-6 col-xs-12', 'order' => $order, 'is_custom' => true, 'special' => null, 'can_change_type' => true, 'can_change_required' => true, 'can_change_visibility' => true, 'can_change_max_length' => true, 'can_change_order' => true, ]; } return $definitions; } private function makeBuiltInField($key, array $overrides = []) { return array_merge([ 'id' => $key, 'key' => $key, 'storage_key' => $key, 'form_name' => $key, 'validation_key' => $key, 'panel' => 'information', 'label' => $key, 'type' => 'text', 'allowed_types' => ['text'], 'required' => false, 'visible' => true, 'max_length' => 255, 'col_class' => 'col-lg-6 col-md-6 col-sm-6 col-xs-6', 'order' => 0, 'is_custom' => false, 'special' => null, 'can_change_type' => true, 'can_change_required' => true, 'can_change_visibility' => true, 'can_change_max_length' => true, 'can_change_order' => true, 'hide_in_form' => false, 'hide_in_show' => false, 'hide_label' => false, ], $overrides); } private function getStoredSettings(User $user) { if (!$this->hasSettingsTable()) { return []; } $record = ContactUserFieldSetting::query() ->where('company_id', $user->company_id) ->whereNull('user_id') ->first(); $settings = $record ? $record->settings : []; return is_array($settings) ? $settings : []; } private function hasSettingsTable() { if ($this->hasSettingsTable === null) { $this->hasSettingsTable = Schema::hasTable('contact_user_field_settings'); } return $this->hasSettingsTable; } private function supportsCustomFields() { if ($this->hasCustomFieldsColumn === null) { $this->hasCustomFieldsColumn = Schema::hasColumn('contacts', 'custom_fields'); } return $this->hasCustomFieldsColumn; } private function applyDependencyAttributes(array $field, array $attr): array { $attr['data-field-key'] = $field['key']; if (!empty($field['depends_on'])) { $attr['data-depends-on'] = $field['depends_on']; $attr['data-depends-map'] = json_encode($field['depends_map'] ?? []); } return $attr; } private function addSimpleField(Form $form, array $field, $value = null) { $formType = $this->mapFormType($field['type']); $options = [ 'label' => $field['label'], ]; $attr = []; if ($field['required']) { $attr['required'] = 'required'; } if ($field['max_length'] !== null && $this->supportsMaxLength($field['type'])) { $attr['maxlength'] = $field['max_length']; } if ($formType === 'textarea') { $attr['rows'] = 3; } if ($field['type'] === 'assigned_to') { $options['choices'] = FormHelper::assignedToChoises(); $options['empty_value'] = trans('core::core.empty_select'); $attr['class'] = trim('select2 pmd-select2 form-control ' . ($attr['class'] ?? '')); $attr = $this->applyDependencyAttributes($field, $attr); } if ($formType === 'select') { if (!isset($options['choices'])) { $options['choices'] = $this->formatSelectChoices($field['options'] ?? []); } if (!isset($options['empty_value'])) { $options['empty_value'] = trans('core::core.empty_select'); } $attr['class'] = trim('select2 pmd-select2 form-control ' . ($attr['class'] ?? '')); $attr = $this->applyDependencyAttributes($field, $attr); } if (!empty($attr)) { $options['attr'] = $attr; } if ($formType === 'checkbox' && $value !== null) { $options['checked'] = (bool) $value; } if ($value !== null && $formType !== 'file') { $options['value'] = $value; } $form->add($field['form_name'], $formType, $options); } private function addSpecialField(Form $form, array $field, User $user, $model = null) { switch ($field['special']) { case 'tags': $form->add($field['form_name'], 'tags', [ 'id' => 'contact-tags', 'value' => FormHelper::getAssignedTags($model), 'label' => $field['label'], ]); break; case 'profile_image': $labelFilename = ''; if ($model && !is_array($model)) { $labelFilename = "\n" . $model->profile_image; } $form->add($field['form_name'], 'file', [ 'label_show' => false, 'attr' => ['id' => 'profile_image', 'class' => 'gravatar_type form-control'], 'label' => $field['label'], 'help_block' => [ 'text' => $labelFilename, 'tag' => 'p', 'attr' => ['class' => 'help-block'], ], ]); break; case 'country_select': $form->add($field['form_name'], 'select', [ 'choices' => Country::whereIsActive(1)->pluck('name', 'id')->toArray(), 'attr' => $this->applyDependencyAttributes($field, ['class' => 'select2 pmd-select2 form-control']), 'label' => $field['label'], 'empty_value' => trans('core::core.empty_select'), ]); break; case 'contact_status_select': $form->add($field['form_name'], 'select', [ 'choices' => ContactStatus::all()->pluck('name', 'id')->toArray(), 'attr' => $this->applyDependencyAttributes($field, ['class' => 'select2 pmd-select2 form-control']), 'label' => $field['label'], 'empty_value' => trans('core::core.empty_select'), ]); break; case 'contact_source_select': $form->add($field['form_name'], 'select', [ 'choices' => ContactSource::all()->pluck('name', 'id')->toArray(), 'attr' => $this->applyDependencyAttributes($field, ['class' => 'select2 pmd-select2 form-control']), 'label' => $field['label'], 'empty_value' => trans('core::core.empty_select'), ]); break; case 'language_select': $form->add($field['form_name'], 'select', [ 'choices' => CustomerLanguage::all()->pluck('name', 'id')->toArray(), 'attr' => $this->applyDependencyAttributes($field, ['class' => 'select2 pmd-select2 form-control']), 'label' => $field['label'], 'empty_value' => trans('core::core.empty_select'), ]); break; case 'account_many_to_one': $form->add($field['form_name'], 'manyToOne', [ 'search_route' => route('accounts.accounts.index', ['mode' => 'modal']), 'relation' => 'account', 'relation_field' => 'name', 'model' => $model, 'modal_title' => 'accounts::accounts.choose', 'attr' => ['class' => 'form-control manyToOne'], 'label' => $field['label'], 'empty_value' => trans('core::core.empty_select'), ]); break; case 'referral_many_to_one': $form->add($field['form_name'], 'manyToOne', [ 'search_route' => route('contacts.contacts.index', ['mode' => 'modal']), 'relation' => 'referral', 'relation_field' => 'full_name', 'model' => $model, 'modal_title' => 'contacts::contacts.choose', 'attr' => ['class' => 'form-control manyToOne'], 'label' => $field['label'], 'empty_value' => trans('core::core.empty_select'), ]); break; case 'owner_select': $form->add($field['form_name'], 'select', [ 'choices' => FormHelper::assignedToChoises(), 'attr' => $this->applyDependencyAttributes($field, ['class' => 'select2 pmd-select2 form-control']), 'label' => $field['label'], 'empty_value' => trans('core::core.empty_select'), 'selected' => FormHelper::assignSelectedFromModel($model), ]); break; } } private function specialShowOptions(array $field) { switch ($field['special']) { case 'tags': return [ 'type' => 'tags', ]; case 'profile_image': return [ 'type' => 'gravatar', ]; case 'country_select': return [ 'type' => 'manyToOne', 'relation' => 'country', 'column' => 'name', ]; case 'contact_status_select': return [ 'type' => 'manyToOne', 'relation' => 'contactStatus', 'column' => 'name', ]; case 'contact_source_select': return [ 'type' => 'manyToOne', 'relation' => 'contactSource', 'column' => 'name', ]; case 'language_select': return [ 'type' => 'manyToOne', 'relation' => 'language', 'column' => 'name', ]; case 'account_many_to_one': return [ 'type' => 'manyToOne', 'relation' => 'account', 'column' => 'name', 'dont_translate' => true, ]; case 'referral_many_to_one': return [ 'type' => 'manyToOne', 'relation' => 'referral', 'column' => 'full_name', 'dont_translate' => true, ]; case 'owner_select': return [ 'type' => 'assigned_to', ]; } return ['type' => 'text']; } private function specialValidationRules(array $field) { $nullableOrRequired = $field['required'] ? ['required'] : ['nullable']; switch ($field['special']) { case 'tags': return [ $field['validation_key'] => array_merge($nullableOrRequired, ['string']), ]; case 'profile_image': return [ $field['validation_key'] => $nullableOrRequired, ]; case 'country_select': return [ $field['validation_key'] => array_merge($nullableOrRequired, ['integer', 'exists:bap_country,id']), ]; case 'contact_status_select': return [ $field['validation_key'] => array_merge($nullableOrRequired, ['integer', 'exists:contacts_dict_status,id']), ]; case 'contact_source_select': return [ $field['validation_key'] => array_merge($nullableOrRequired, ['integer', 'exists:contacts_dict_source,id']), ]; case 'language_select': return [ $field['validation_key'] => array_merge($nullableOrRequired, ['integer', 'exists:customer_dict_language,id']), ]; case 'account_many_to_one': return [ $field['validation_key'] => array_merge($nullableOrRequired, ['integer', 'exists:accounts,id']), ]; case 'referral_many_to_one': return [ $field['validation_key'] => array_merge($nullableOrRequired, ['integer', 'exists:contacts,id']), ]; case 'owner_select': return [ $field['validation_key'] => array_merge($nullableOrRequired, ['string', 'max:255']), ]; } return []; } private function simpleValidationRules(array $field) { $rules = $field['required'] ? ['required'] : ['nullable']; switch ($field['type']) { case 'checkbox': $rules[] = 'boolean'; break; case 'file': break; case 'number': $rules[] = 'numeric'; break; case 'email': $rules[] = 'email'; break; case 'url': $rules[] = 'url'; break; case 'date': case 'datetime': $rules[] = 'date'; break; case 'select': case 'manytoone': $rules[] = 'string'; $allowed = $this->normalizeOptions($field['options'] ?? []); if (!empty($allowed)) { $rules[] = Rule::in($allowed); } break; case 'assigned_to': case 'tags': $rules[] = 'string'; break; default: $rules[] = 'string'; break; } if ($field['max_length'] !== null && $this->supportsMaxLength($field['type'])) { $rules[] = 'max:' . $field['max_length']; } return $rules; } private function mapFormType($type) { switch ($type) { case 'textarea': return 'textarea'; case 'number': return 'number'; case 'checkbox': return 'checkbox'; case 'date': return 'dateType'; case 'datetime': return 'dateTimeType'; case 'tags': return 'tags'; case 'file': return 'file'; case 'select': return 'select'; case 'assigned_to': case 'manytoone': return 'select'; default: return 'text'; } } private function mapShowType($type) { switch ($type) { case 'email': return 'email'; case 'number': return 'number'; case 'date': return 'date'; case 'datetime': return 'datetime'; case 'checkbox': return 'checkbox'; case 'file': return 'file'; case 'assigned_to': return 'assigned_to'; default: return 'text'; } } private function supportsOptionsForType($type): bool { return in_array($type, ['select', 'manytoone'], true); } private function isSelectableSpecial($special): bool { return in_array($special, [ 'country_select', 'contact_status_select', 'contact_source_select', 'language_select', 'owner_select', ], true); } private function isSelectableDefinition($type, $special): bool { if (!empty($special)) { return $this->isSelectableSpecial($special); } return in_array($type, ['select', 'assigned_to', 'manytoone'], true); } private function isSelectableField(array $field): bool { return $this->isSelectableDefinition($field['type'] ?? null, $field['special'] ?? null); } private function collectSelectableKeys(array $defaults, array $builtIn, array $custom): array { $keys = []; foreach ($defaults as $key => $field) { $type = $builtIn[$key]['type'] ?? $field['type']; if ($this->isSelectableDefinition($type, $field['special'] ?? null)) { $keys[] = $key; } } foreach ($custom as $field) { if ($this->isSelectableDefinition($field['type'] ?? null, $field['special'] ?? null)) { $keys[] = $field['key']; } } return $keys; } private function normalizeFieldDependencies(array $fields): array { $selectableKeys = []; foreach ($fields as $field) { if ($this->isSelectableField($field)) { $selectableKeys[] = $field['key']; } } foreach ($fields as &$field) { $dependsOn = trim((string) ($field['depends_on'] ?? '')); $dependsMap = $this->normalizeDependencyMap($field['depends_map'] ?? []); if (!$this->isSelectableField($field)) { $field['depends_on'] = null; $field['depends_map'] = []; continue; } if ($dependsOn === '' || $dependsOn === $field['key'] || !in_array($dependsOn, $selectableKeys, true)) { $field['depends_on'] = null; $field['depends_map'] = $dependsMap; continue; } $field['depends_on'] = $dependsOn; $field['depends_map'] = $dependsMap; } unset($field); return $fields; } private function normalizeDependencyParent($value, array $selectableKeys, string $childKey): ?string { $parent = trim((string) $value); if ($parent === '' || $parent === $childKey) { return null; } if (!in_array($parent, $selectableKeys, true)) { return null; } return $parent; } private function normalizeDependencyMap($raw): array { if (is_array($raw)) { return $this->sanitizeDependencyMap($raw); } // The global XSSProtection middleware runs HTMLPurifier on all POST // input, which encodes => as => in plain-text values. // Decode HTML entities so the arrow separator is recognised. $rawString = trim(html_entity_decode((string) $raw, ENT_QUOTES | ENT_HTML5, 'UTF-8')); if ($rawString === '') { return []; } if ($rawString[0] === '{' || $rawString[0] === '[') { $decoded = json_decode($rawString, true); if (is_array($decoded)) { return $this->sanitizeDependencyMap($decoded); } } $map = []; $lines = preg_split('/\r\n|\r|\n/', $rawString) ?: []; foreach ($lines as $line) { $line = trim($line); if ($line === '') { continue; } $parts = preg_split('/\s*=>\s*|\s*:\s*/', $line, 2); if (!$parts || count($parts) < 2) { continue; } $parent = trim($parts[0]); $childrenRaw = trim($parts[1]); if ($parent === '' || $childrenRaw === '') { continue; } $children = preg_split('/[\r\n,]+/', $childrenRaw) ?: []; $map[$parent] = $children; } return $this->sanitizeDependencyMap($map); } private function sanitizeDependencyMap(array $map): array { $result = []; foreach ($map as $parent => $children) { $parentKey = trim((string) $parent); if ($parentKey === '') { continue; } if (!is_array($children)) { $children = preg_split('/[\r\n,]+/', (string) $children) ?: []; } $values = []; foreach ($children as $child) { $value = trim((string) $child); if ($value !== '') { $values[] = $value; } } if (!empty($values)) { $result[$parentKey] = array_values(array_unique($values)); } } return $result; } public function validateDependencies(User $user, array $input): array { $errors = []; $fields = $this->getEffectiveFields($user); $fieldMap = []; foreach ($fields as $field) { $fieldMap[$field['key']] = $field; } foreach ($fields as $field) { if (empty($field['depends_on']) || !$this->isSelectableField($field)) { continue; } $parentField = $fieldMap[$field['depends_on']] ?? null; if (!$parentField || !$this->isSelectableField($parentField)) { continue; } $childValue = $this->getDependencyInputValue($input, $field); $childValues = $this->normalizeDependencyValues($childValue); if (empty($childValues)) { continue; } $parentValue = $this->getDependencyInputValue($input, $parentField); $parentValues = $this->normalizeDependencyValues($parentValue); if (empty($parentValues)) { $this->addDependencyError($errors, $field, $parentField, 'Please select ' . $parentField['label'] . ' before choosing ' . $field['label'] . '.'); continue; } $map = is_array($field['depends_map'] ?? null) ? $field['depends_map'] : []; if (empty($map)) { continue; } $allowed = $this->resolveAllowedDependencyValues($map, $parentValues); if (empty($allowed)) { continue; } $allowed = array_map('strval', $allowed); $invalidValues = array_diff(array_map('strval', $childValues), $allowed); if (!empty($invalidValues)) { $this->addDependencyError($errors, $field, $parentField, 'Selected value is not allowed for ' . $parentField['label'] . '.'); } } return $errors; } private function getDependencyInputValue(array $input, array $field) { $key = $field['validation_key'] ?? $field['key']; return Arr::get($input, $key); } private function normalizeDependencyValues($value): array { if (is_array($value)) { return array_values(array_filter(array_map('strval', $value), static function ($item) { return $item !== ''; })); } if ($value === null || $value === '') { return []; } return [(string) $value]; } private function resolveAllowedDependencyValues(array $map, array $parentValues): array { $allowed = []; foreach ($parentValues as $parentValue) { $options = $map[(string) $parentValue] ?? null; if (!is_array($options) || empty($options)) { continue; } foreach ($options as $option) { $allowed[] = (string) $option; } } return array_values(array_unique(array_filter($allowed, static function ($value) { return $value !== ''; }))); } private function isEmptyDependencyValue($value): bool { return $value === null || $value === ''; } private function addDependencyError(array &$errors, array $field, array $parentField, string $message): void { $key = $field['validation_key'] ?? $field['key']; if (!isset($errors[$key])) { $errors[$key] = []; } $errors[$key][] = $message; } private function supportsMaxLength($type) { return in_array($type, ['text', 'textarea', 'email', 'tel', 'url', 'tags'], true); } private function normalizeOptions($value): array { $raw = []; if (is_string($value)) { $raw = preg_split('/[\r\n,]+/', $value) ?: []; } elseif (is_array($value)) { $raw = $value; } $options = []; foreach ($raw as $item) { $option = trim((string) $item); if ($option !== '') { $options[] = $option; } } $options = array_values(array_unique($options)); if (count($options) > 200) { $options = array_slice($options, 0, 200); } return $options; } private function formatSelectChoices(array $options): array { $choices = []; foreach ($this->normalizeOptions($options) as $option) { $choices[$option] = $option; } return $choices; } private function normalizeBool($value) { return in_array($value, [true, 1, '1', 'true', 'on', 'yes'], true); } private function normalizeType($value, array $allowedTypes, $fallback) { $value = strtolower(trim((string) $value)); if (in_array($value, $allowedTypes, true)) { return $value; } return $fallback; } private function normalizePanel($value) { $panel = trim((string) $value); if ($panel === self::LEGACY_CUSTOM_PANEL) { $panel = self::DEFAULT_CUSTOM_PANEL; } if (in_array($panel, self::EDITABLE_PANELS, true)) { return $panel; } return self::DEFAULT_CUSTOM_PANEL; } private function normalizeOrder($value, $fallback) { if ($value === null || $value === '') { return (int) $fallback; } $order = (int) $value; if ($order < 0) { return (int) $fallback; } if ($order > 1000000) { return 1000000; } return $order; } private function normalizeMaxLength($value, $type) { if (!$this->supportsMaxLength($type)) { return null; } if ($value === null || $value === '') { return null; } $max = (int) $value; if ($max <= 0) { return null; } if ($max > 10000) { return 10000; } return $max; } private function sanitizeCustomKey($raw) { $key = (string) Str::of((string) $raw) ->replaceMatches('/[^A-Za-z0-9_\-\s]/', '') ->snake() ->trim('_'); return $key; } private function getTypeOptionsForAllowedTypes(array $allowedTypes) { $options = []; foreach ($allowedTypes as $type) { if (isset(self::TYPE_OPTIONS[$type])) { $options[$type] = self::TYPE_OPTIONS[$type]; } } return $options; } private function defaultPanelLabels() { $options = []; foreach (self::EDITABLE_PANELS as $panel) { $translationKey = 'contacts::contacts.panel.' . $panel; $label = trans($translationKey); if ($label === $translationKey) { $label = Str::title(str_replace('_', ' ', $panel)); } $options[$panel] = $label; } return $options; } /** * Normalise submitted panel_labels[] values. Only known panels are kept; * blank values are dropped so the module falls back to the default title. */ private function normalizePanelLabels($input) { $labels = []; $defaults = $this->defaultPanelLabels(); if (is_array($input)) { foreach (self::EDITABLE_PANELS as $panel) { $value = trim((string) Arr::get($input, $panel, '')); if ($value === '' || $value === ($defaults[$panel] ?? null)) { continue; } $labels[$panel] = mb_substr($value, 0, 120); } } return $labels; } /** * Effective panel titles (defaults merged with any saved custom titles) for * the CRM settings editor and the shared panel drop-downs. */ public function getPanelLabels(User $user) { return array_merge($this->defaultPanelLabels(), $this->getPanelLabelOverrides($user)); } /** * Saved custom panel titles only (used by the shared Html::section renderer * so renamed titles appear on the real create/edit/show pages). */ public function getPanelLabelOverrides(User $user) { $settings = $this->getStoredSettings($user); $stored = Arr::get($settings, 'panel_labels', []); if (!is_array($stored)) { return []; } $overrides = []; foreach (self::EDITABLE_PANELS as $panel) { if (isset($stored[$panel]) && trim((string) $stored[$panel]) !== '') { $overrides[$panel] = trim((string) $stored[$panel]); } } return $overrides; } }