'contacts.browse', 'create' => 'contacts.create', 'update' => 'contacts.update', 'destroy' => 'contacts.destroy' ]; protected $showRoute = 'contacts.contacts.show'; protected $storeRequest = ContactApiRequest::class; protected $updateRequest = ContactApiRequest::class; public function __construct() { parent::__construct(); } /** * Create a contact, including provisioned custom-field column values. */ public function store() { $this->checkCompanyContext(); $request = \App::make($this->storeRequest ?? Request::class); if ($this->permissions['create'] != '' && !\Auth::user()->hasPermissionTo($this->permissions['create'])) { return $this->respond(false, [], ['error' => 'user_dont_no_access'], ['message' => trans('core::core.you_dont_have_access')]); } $repository = $this->getRepository(); $entityClass = \App::make($this->entityClass); $fillableData = $request->only($entityClass->getFillable()); // Merge provisioned custom-field column values not in $fillable $user = \Auth::user(); $fieldService = app(ContactFieldSettingsService::class); foreach ($fieldService->getCustomFieldKeys($user) as $key) { if ($request->has($key) && !array_key_exists($key, $fillableData)) { $fillableData[$key] = $request->input($key); } } $entity = $repository->createEntity($fillableData, $entityClass); $entity = $this->setupAssignedTo($entity, $request->all(), true); $entity->save(); $contact = $repository->with($this->with)->findWithoutFail($entity->id); $this->entity = $contact; return $this->respond(true, $this->prepareContactForResponse($contact), [], ['message' => trans('core::core.entity.created')]); } /** * Update a contact. Provisioned custom-field columns are written directly. * Orphaned (deleted from settings) column values are hidden from the response. */ public function update($identifier) { $this->checkCompanyContext(); $request = \App::make($this->updateRequest ?? Request::class); if ($this->permissions['update'] != '' && !\Auth::user()->hasPermissionTo($this->permissions['update'])) { return $this->respond(false, [], ['error' => 'user_dont_no_access'], ['message' => trans('core::core.you_dont_have_access')]); } $repository = $this->getRepository(); $entity = $repository->findWithoutFail($identifier); $this->entity = $entity; if (empty($entity)) { return $this->respond(false, [], ['error' => 'entity_not_found'], ['message' => trans('core::core.entity.entity_not_found')]); } if ($this->blockEntityOwnableAccess()) { return $this->respond(false, [], ['error' => 'user_dont_no_access'], ['message' => trans('core::core.you_dont_have_access')]); } $input = $request->all(); $entity = $this->setupAssignedTo($entity, $input); $repository->updateEntity($input, $entity); $contact = $repository->with($this->with)->findWithoutFail($identifier); $this->entity = $contact; return $this->respond(true, $this->prepareContactForResponse($contact), [], ['message' => trans('core::core.entity.updated')]); } /** * Get contact details with field metadata (mirrors leads/get/{id}). */ public function get($identifier) { $this->checkCompanyContext(); if ($this->permissions['browse'] != '' && !\Auth::user()->hasPermissionTo($this->permissions['browse'])) { return $this->respond(false, [], ['error' => 'user_dont_no_access'], ['message' => trans('core::core.you_dont_have_access')]); } $repository = $this->getRepository(); $entity = $repository->with($this->with)->findWithoutFail($identifier); if (empty($entity)) { return $this->respond(false, [], ['error' => 'entity_not_found'], ['message' => trans('core::core.entity.entity_not_found')]); } if ($this->blockEntityOwnableAccess()) { return $this->respond(false, [], ['error' => 'user_dont_no_access'], ['message' => trans('core::core.you_dont_have_access')]); } $user = \Auth::user(); $fieldService = app(ContactFieldSettingsService::class); $visibleFields = $fieldService->getVisibleFieldsForApi($user); $validationRules = $fieldService->getValidationRules($user); $optionsMap = $this->getContactFieldOptionsMap($user->company_id); $rules = $this->buildContactFieldRules($visibleFields, $optionsMap); $fieldEntries = []; $fieldMap = []; foreach ($visibleFields as $field) { $metadata = $this->buildContactFieldMetadata($entity, $field, $validationRules, $optionsMap); $fieldEntries[] = $metadata; $fieldMap[$metadata['key']] = $metadata; } return $this->respond(true, [ 'contact' => $this->prepareContactForResponse($entity), 'fields' => $fieldEntries, 'field_map' => $fieldMap, 'rules' => $rules, ], [], ['message' => trans('core::core.entity.entity_found')]); } /** * Prepare a Contact for API response: * - Hide provisioned-column keys deleted from CRM settings. * - Remove stale custom_fields JSON entries for column-backed keys. */ private function prepareContactForResponse(Contact $contact): array { $user = \Auth::user(); $fieldService = app(ContactFieldSettingsService::class); $currentCustomKeys = $fieldService->getCustomFieldKeys($user); $provisionedKeys = $fieldService->getProvisionedColumnKeys($user); $orphanedKeys = array_diff($provisionedKeys, $currentCustomKeys); $data = $contact->toArray(); foreach ($orphanedKeys as $key) { unset($data[$key]); } // Flatten remaining custom_fields JSON entries into top-level keys, // then unconditionally remove the raw column from the response. if (is_array($data['custom_fields'] ?? null)) { foreach ($provisionedKeys as $key) { unset($data['custom_fields'][$key]); } foreach ($data['custom_fields'] as $key => $value) { $data[$key] = $value; } } unset($data['custom_fields']); // always removed — internal storage detail // Remove internal / sensitive columns unset($data['deleted_at']); return $data; } /** * Build detail metadata for a single visible contact field. */ private function buildContactFieldMetadata(Contact $contact, array $field, array $validationRules, array $optionsMap): array { $key = $field['key']; $validationKey = $field['validation_key'] ?? $key; $fieldRules = $validationRules[$validationKey] ?? Arr::get($validationRules, $key) ?? []; $validation = is_array($fieldRules) ? $fieldRules : explode('|', $fieldRules); $value = $this->resolveContactFieldValue($contact, $field); $metadata = [ 'key' => $key, 'label' => $field['label'], 'type' => $field['special'] !== null ? 'select' : ($field['type'] ?? 'text'), 'input_type' => $field['type'] ?? 'text', 'required' => (bool) $field['required'], 'nullable' => !$field['required'], 'visible' => (bool) ($field['visible'] ?? true), 'is_custom' => (bool) ($field['is_custom'] ?? false), 'panel' => $field['panel'] ?? null, 'order' => $field['order'] ?? null, 'special' => $field['special'] ?? null, 'max_length' => $field['max_length'] ?? null, 'value' => $value, 'display_value' => $value, 'validation' => $validation, ]; if (!empty($field['depends_on'])) { $metadata['depends_on'] = $field['depends_on']; $metadata['depends_map'] = $field['depends_map'] ?? []; } if (array_key_exists($key, $optionsMap)) { $metadata['options'] = $optionsMap[$key]; } if (!array_key_exists($key, $optionsMap) && ($field['type'] ?? null) === 'select') { $customOptions = $this->normalizeCustomSelectOptions($field['options'] ?? []); if (!empty($customOptions)) { $metadata['options'] = $customOptions; } } return $metadata; } /** * Build grouped dependency rules for API clients. */ private function buildContactFieldRules(array $fields, array $optionsMap): array { $fieldMap = []; foreach ($fields as $field) { $fieldMap[$field['key']] = $field; } $rules = []; foreach ($fields as $field) { $dependsOn = trim((string) ($field['depends_on'] ?? '')); if ($dependsOn === '') { continue; } $parentField = $fieldMap[$dependsOn] ?? ['key' => $dependsOn, 'label' => $dependsOn]; $rules[] = [ 'field' => $field['key'], 'field_label' => $field['label'], 'when_field' => $dependsOn, 'when_field_label' => $parentField['label'] ?? $dependsOn, 'conditions' => $this->buildContactRuleConditions( is_array($field['depends_map'] ?? null) ? $field['depends_map'] : [], $this->getContactRuleOptions($parentField, $optionsMap), $this->getContactRuleOptions($field, $optionsMap) ), ]; } return $rules; } private function buildContactRuleConditions(array $dependsMap, array $parentOptions, array $childOptions): array { $conditions = []; foreach ($dependsMap as $parentValue => $allowedValues) { if (!is_array($allowedValues)) { continue; } $conditions[] = [ 'value' => (string) $parentValue, 'display_value' => $this->resolveContactRuleDisplayValue($parentValue, $parentOptions), 'options' => $this->filterContactRuleOptions($allowedValues, $childOptions), ]; } return array_values($conditions); } private function getContactRuleOptions(array $field, array $optionsMap): array { $key = $field['key'] ?? null; if ($key !== null && array_key_exists($key, $optionsMap)) { return array_values($optionsMap[$key]); } if (($field['type'] ?? null) === 'select') { return $this->normalizeCustomSelectOptions($field['options'] ?? []); } return []; } private function filterContactRuleOptions(array $allowedValues, array $availableOptions): array { $lookup = []; foreach ($availableOptions as $option) { $optionId = Arr::get($option, 'id', Arr::get($option, 'value')); if ($optionId === null || $optionId === '') { continue; } $lookup[(string) $optionId] = [ 'id' => $optionId, 'name' => Arr::get($option, 'name', Arr::get($option, 'label', (string) $optionId)), ]; } $result = []; $seen = []; foreach ($allowedValues as $allowedValue) { $allowedKey = trim((string) $allowedValue); if ($allowedKey === '' || isset($seen[$allowedKey])) { continue; } $seen[$allowedKey] = true; $result[] = $lookup[$allowedKey] ?? ['id' => $allowedKey, 'name' => $allowedKey]; } return $result; } private function resolveContactRuleDisplayValue($value, array $availableOptions): string { $valueKey = (string) $value; foreach ($availableOptions as $option) { $optionId = Arr::get($option, 'id', Arr::get($option, 'value')); if ((string) $optionId === $valueKey) { return (string) (Arr::get($option, 'name', Arr::get($option, 'label', $valueKey)) ?? $valueKey); } } return $valueKey; } /** * Resolve raw value for a contact field. */ private function resolveContactFieldValue(Contact $contact, array $field) { $key = $field['key']; if (!empty($field['is_custom'])) { if (!empty($field['uses_column'])) { return $contact->{$key} ?? null; } $custom = is_array($contact->custom_fields) ? $contact->custom_fields : []; return $custom[$key] ?? null; } $relationMap = [ 'contact_status_id' => ['contactStatus', 'name'], 'contact_source_id' => ['contactSource', 'name'], 'language_id' => ['language', 'name'], 'country_id' => ['country', 'name'], 'account_id' => ['account', 'name'], 'referral_id' => ['referral', 'full_name'], ]; if (isset($relationMap[$key])) { [$relation, $column] = $relationMap[$key]; return optional($contact->{$relation})->{$column}; } return $contact->{$key} ?? null; } /** * Build options map for select-type fields. */ private function getContactFieldOptionsMap(int $companyId): array { return [ 'contact_status_id' => ContactStatus::where('company_id', $companyId)->get(['id', 'name'])->toArray(), 'contact_source_id' => ContactSource::where('company_id', $companyId)->get(['id', 'name'])->toArray(), 'language_id' => CustomerLanguage::get(['id', 'name'])->toArray(), 'country_id' => Country::get(['id', 'name'])->toArray(), 'account_id' => Account::where('company_id', $companyId)->get(['id', 'name'])->toArray(), ]; } private function normalizeCustomSelectOptions($options): array { $raw = []; if (is_string($options)) { $raw = preg_split('/[\r\n,]+/', $options) ?: []; } elseif (is_array($options)) { $raw = $options; } $result = []; foreach ($raw as $item) { $value = trim((string) $item); if ($value === '') { continue; } $result[$value] = ['id' => $value, 'name' => $value]; } return array_values($result); } /** * List contacts with optional filters. * * Supported query parameters: * - search : searches full_name, phone/mobile, emails, job title, department, account name * - contact_status_id : filter by status ID (int, array or comma-separated IDs) * - contact_source_id : filter by source ID * - language_id : filter by language ID * - country_id : filter by country ID * - account_id : filter by account ID * - referral_id : filter by referral contact ID * - first_name : partial first name * - last_name : partial last name * - full_name : partial full name * - email : partial primary/secondary email * - phone : partial phone * - mobile : partial mobile * - job_title : partial job title * - department : partial department * - assistant_name : partial assistant name * - assistant_phone : partial assistant phone * - street : partial street * - city : partial city * - state : partial state * - zip_code : partial zip/postal code * - account_name : partial account name * - created_from : created on/after date (Y-m-d) * - created_to : created on/before date (Y-m-d) * - birth_date_from : birth date on/after date (Y-m-d) * - birth_date_to : birth date on/before date (Y-m-d) * - per_page : records per page (default 15) * * @param Request $request * @return \Illuminate\Http\JsonResponse */ public function index(Request $request) { $this->checkCompanyContext(); $query = \App::make($this->entityClass)->with($this->with); if ($this->scopedAccess()) { $scope = new OwnableEntityScope(\Auth::user(), $this->moduleName); $query = $scope->apply($query); } if ($search = $request->get('search')) { $term = '%' . $search . '%'; $query->where(function ($q) use ($term) { $q->where('first_name', 'like', $term) ->orWhere('last_name', 'like', $term) ->orWhere('full_name', 'like', $term) ->orWhere('phone', 'like', $term) ->orWhere('mobile', 'like', $term) ->orWhere('email', 'like', $term) ->orWhere('job_title', 'like', $term) ->orWhere('department', 'like', $term) ->orWhereHas('contactEmails', function ($emailQuery) use ($term) { $emailQuery->where('email', 'like', $term); }) ->orWhereHas('account', function ($accountQuery) use ($term) { $accountQuery->where('name', 'like', $term); }); }); } $applyIdFilter = function ($column, $parameter) use ($request, $query) { if (!$request->filled($parameter)) { return; } $rawIds = $request->get($parameter); if (!is_array($rawIds)) { $rawIds = explode(',', (string)$rawIds); } $ids = array_values(array_filter($rawIds, function ($id) { return $id !== null && $id !== ''; })); if (!empty($ids)) { $query->whereIn($column, $ids); } }; $applyIdFilter('contact_status_id', 'contact_status_id'); $applyIdFilter('contact_source_id', 'contact_source_id'); $applyIdFilter('language_id', 'language_id'); $applyIdFilter('country_id', 'country_id'); $applyIdFilter('account_id', 'account_id'); $applyIdFilter('referral_id', 'referral_id'); $likeFilters = [ 'first_name' => 'first_name', 'last_name' => 'last_name', 'full_name' => 'full_name', 'phone' => 'phone', 'mobile' => 'mobile', 'job_title' => 'job_title', 'department' => 'department', 'assistant_name' => 'assistant_name', 'assistant_phone' => 'assistant_phone', 'street' => 'street', 'city' => 'city', 'state' => 'state', 'zip_code' => 'zip_code', ]; foreach ($likeFilters as $parameter => $column) { if ($request->filled($parameter)) { $query->where($column, 'like', '%' . $request->get($parameter) . '%'); } } if ($request->filled('email')) { $term = '%' . $request->get('email') . '%'; $query->where(function ($q) use ($term) { $q->where('email', 'like', $term) ->orWhereHas('contactEmails', function ($emailQuery) use ($term) { $emailQuery->where('email', 'like', $term); }); }); } if ($request->filled('account_name')) { $term = '%' . $request->get('account_name') . '%'; $query->whereHas('account', function ($accountQuery) use ($term) { $accountQuery->where('name', 'like', $term); }); } if ($request->filled('created_from')) { $query->whereDate('created_at', '>=', $request->get('created_from')); } if ($request->filled('created_to')) { $query->whereDate('created_at', '<=', $request->get('created_to')); } if ($request->filled('birth_date_from')) { $query->whereDate('birth_date', '>=', $request->get('birth_date_from')); } if ($request->filled('birth_date_to')) { $query->whereDate('birth_date', '<=', $request->get('birth_date_to')); } $perPage = $request->get('per_page', self::DEFAULT_PER_PAGE); $paginated = $query->paginate((int)$perPage); $filterKeys = [ 'search', 'contact_status_id', 'contact_source_id', 'language_id', 'country_id', 'account_id', 'referral_id', 'first_name', 'last_name', 'full_name', 'email', 'phone', 'mobile', 'job_title', 'department', 'assistant_name', 'assistant_phone', 'street', 'city', 'state', 'zip_code', 'account_name', 'created_from', 'created_to', 'birth_date_from', 'birth_date_to', ]; $filtersApplied = collect($filterKeys)->contains(function ($key) use ($request) { return $request->filled($key); }); if ($paginated->total() === 0 && $filtersApplied) { return $this->respond(true, [['data' => [], 'total' => 0]], [], ['message' => 'No records match the applied filters.']); } // Return full contact records with index_fields appended as supplementary data $user = \Auth::user(); $fieldService = app(ContactFieldSettingsService::class); $apiIndexFields = $fieldService->getApiIndexFields($user); $paginatedArray = $paginated->toArray(); $paginatedArray['data'] = array_map(function ($item) use ($apiIndexFields, $paginated) { $contact = $paginated->getCollection()->firstWhere('id', $item['id']); $indexFields = []; if ($contact) { foreach ($apiIndexFields as $fieldDef) { $indexFields[] = [ 'key' => $fieldDef['key'], 'label' => $fieldDef['label'], 'value' => $this->resolveContactFieldValue($contact, $fieldDef), ]; } } $record = $contact ? $this->prepareContactForResponse($contact) : ['id' => $item['id']]; $record['index_fields'] = $indexFields; return $record; }, $paginatedArray['data']); return $this->respond(true, [$paginatedArray], [], ['message' => trans('core::core.entity.records_found')]); } /** * Return field definitions for data-driven mobile UI. * * Uses the same rich metadata contract as get/{id} but without value/display_value. * * @return \Illuminate\Http\JsonResponse */ public function fields() { $this->checkCompanyContext(); $user = \Auth::user(); $companyId = $user->company_id; $fieldSettingsService = app(ContactFieldSettingsService::class); $visibleFields = $fieldSettingsService->getVisibleFieldsForApi($user); $validationRules = $fieldSettingsService->getValidationRules($user); $optionsMap = $this->getContactFieldOptionsMap($companyId); $rules = $this->buildContactFieldRules($visibleFields, $optionsMap); $fieldEntries = []; $fieldMap = []; foreach ($visibleFields as $field) { $key = $field['key']; $validationKey = $field['validation_key'] ?? $key; $validation = $this->normalizeValidationRules( $validationRules[$validationKey] ?? Arr::get($validationRules, $key) ); $apiType = $this->mapApiFieldType($field); $metadata = [ 'key' => $key, 'label' => $field['label'], 'type' => $apiType, 'input_type' => $field['type'] ?? $apiType, 'required' => (bool) $field['required'], 'nullable' => !$field['required'], 'visible' => true, 'is_custom' => (bool) ($field['is_custom'] ?? false), 'panel' => $field['panel'] ?? null, 'order' => $field['order'] ?? null, 'special' => $field['special'] ?? null, 'max_length' => $field['max_length'] ?? null, 'validation' => $validation, ]; if (!empty($field['depends_on'])) { $metadata['depends_on'] = $field['depends_on']; $metadata['depends_map'] = $field['depends_map'] ?? []; } if (array_key_exists($key, $optionsMap)) { $metadata['options'] = $optionsMap[$key]; } if (!array_key_exists($key, $optionsMap) && ($field['type'] ?? null) === 'select') { $customOptions = $this->normalizeCustomSelectOptions($field['options'] ?? []); if (!empty($customOptions)) { $metadata['options'] = $customOptions; } } $fieldEntries[] = $metadata; $fieldMap[$metadata['key']] = $metadata; } return $this->respond(true, [ 'fields' => $fieldEntries, 'field_map' => $fieldMap, 'rules' => $rules, ], [], ['message' => 'fields fetched successfully']); } /** * Map a CRM settings field type to a user-friendly API type. */ private function mapApiFieldType(array $field) { if ($field['special'] !== null) { // Special relation/select fields return 'select'; } return $field['type'] ?? 'text'; } /** * Normalize validation rules to array format. */ private function normalizeValidationRules($rules) { if (is_string($rules)) { return array_values(array_filter(explode('|', $rules))); } if (is_array($rules)) { return array_values($rules); } return []; } public function allStatuses() { $choices = ContactStatus::where('company_id', \Auth::user()->company_id) ->pluck('name', 'id') ->toArray(); return $this->respond(true, ['user' => $choices], [], 'data fetched successfully'); } public function allSources() { $choices = ContactSource::where('company_id', \Auth::user()->company_id) ->pluck('name', 'id') ->toArray(); return $this->respond(true, ['user' => $choices], [], 'data fetched successfully'); } }