diff --git a/app/Livewire/ClientsTable.php b/app/Livewire/ClientsTable.php index c5b9c53..62c537c 100644 --- a/app/Livewire/ClientsTable.php +++ b/app/Livewire/ClientsTable.php @@ -116,7 +116,11 @@ function getClients() } if (array_key_exists('search', $this->filter) && $searchString = trim($this->filter['search'])) { - $clients->whereFullText(['name', 'phone', 'email'], $this->filter['search']); + $searchString = mb_strtolower(trim($this->filter['search'])); + $clients->whereFullText(['name', 'phone', 'email'], $searchString); + $clients->orWhere('normalized_name', 'like', "%{$searchString}%") + ->orWhere('normalized_phone', 'like', "%{$searchString}%") + ->orWhere('email', 'like', "%{$searchString}%"); } if (array_key_exists('status', $this->filter) && $this->status == DealStatus::UNIQUE) { diff --git a/app/Modules/Main/Models/Deal/Client.php b/app/Modules/Main/Models/Deal/Client.php index 50bd3ec..3c03408 100644 --- a/app/Modules/Main/Models/Deal/Client.php +++ b/app/Modules/Main/Models/Deal/Client.php @@ -6,10 +6,20 @@ use Modules\Main\Models\Agent\Agent; use Modules\Bitrix\Traits\Bitrixable; + +use Laravel\Scout\Searchable; class Client extends User { - use Bitrixable; + use Bitrixable, Searchable; protected $table = 'users'; + public function toSearchableArray() + { + return [ + 'name' => $this->name, + 'phone' => $this->price, + 'email' => $this->email, + ]; + } public function deals() { return $this->hasManyThrough( diff --git a/app/Modules/User/Database/Migrations/2026_05_06_000001_add_normalized_name_and_phone_fields_to_users_table.php b/app/Modules/User/Database/Migrations/2026_05_06_000001_add_normalized_name_and_phone_fields_to_users_table.php new file mode 100644 index 0000000..8fff2e5 --- /dev/null +++ b/app/Modules/User/Database/Migrations/2026_05_06_000001_add_normalized_name_and_phone_fields_to_users_table.php @@ -0,0 +1,44 @@ +string('normalized_name')->nullable()->after('name'); + $table->string('normalized_phone')->nullable()->after('phone'); + }); + DB::statement('UPDATE users SET normalized_name = LOWER(name) + , normalized_phone = + REPLACE( + REPLACE( + REPLACE( + REPLACE( + REPLACE(phone, " ", "") + , "-", "") + , "+", "") + , "(", "") + , ")", "") + '); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::table('users', function (Blueprint $table) + { + $table->dropColumn('normalized_name'); + $table->dropColumn('normalized_phone'); + }); + } +}; diff --git a/app/Modules/User/Models/User.php b/app/Modules/User/Models/User.php index 329924d..3d8c5cf 100644 --- a/app/Modules/User/Models/User.php +++ b/app/Modules/User/Models/User.php @@ -9,12 +9,14 @@ use Illuminate\Database\Eloquent\Relations\HasMany; use Illuminate\Database\Eloquent\Relations\HasManyThrough; use Illuminate\Database\Eloquent\Factories\HasFactory; +use Laravel\Scout\Searchable; +use Laravel\Scout\Attributes\SearchUsingFullText; use Illuminate\Foundation\Auth\User as Authenticatable; class User extends Authenticatable { - use HasApiTokens, HasFactory, Notifiable, ForcedPassword; + use HasApiTokens, HasFactory, Notifiable, ForcedPassword, Searchable; /** * The attributes that are mass assignable. @@ -23,7 +25,9 @@ class User extends Authenticatable */ protected $fillable = [ 'name', + 'normalized_name', 'phone', + 'normalized_phone', 'email', 'password', ]; @@ -48,6 +52,15 @@ class User extends Authenticatable 'password' => 'hashed', ]; + #[SearchUsingFullText(['name', 'phone', 'email'])] + public function toSearchableArray() + { + return [ + 'name' => $this->name, + 'phone' => $this->price, + 'email' => $this->email, + ]; + } public function getPartialsName() { $name = explode(' ', $this->name); @@ -65,8 +78,7 @@ public function getPartialsName() public function hasRole($roleId) { $roles = $this->roles()->get(); - if ($roles->where('id', $roleId)->count()) - { + if ($roles->where('id', $roleId)->count()) { return true; } return false; @@ -89,16 +101,11 @@ public function isCityManager() public function roles(): HasManyThrough { return $this->hasManyThrough( - Role::class - , - UserRole::class - , - 'user_id' - , - 'id' - , - 'id' - , + Role::class, + UserRole::class, + 'user_id', + 'id', + 'id', 'role_id' ); @@ -106,12 +113,16 @@ public function roles(): HasManyThrough protected static function booted() { - static::deleted(function (User $user) - { + static::creating(function (User $user) { + $user->normalized_name = mb_strtolower($user->name); + $user->normalized_phone = str_replace('+7', '8', $user->phone); + $user->normalized_phone = str_replace([' ', '-', '+', '(', ')'], '', $user->normalized_phone); + }); + + static::deleted(function (User $user) { foreach ($userRoles = UserRole::where('user_id', $user->id)->get() as $userRole) { $userRole->delete(); } }); } - } diff --git a/composer.json b/composer.json index 3f70498..0cdd6c3 100644 --- a/composer.json +++ b/composer.json @@ -13,6 +13,7 @@ "laravel/fortify": "^1.24", "laravel/framework": "^10.10", "laravel/sanctum": "^3.3", + "laravel/scout": "^11.1", "laravel/tinker": "^2.8", "laravel/ui": "^4.5", "livewire/livewire": "^3.5" @@ -70,4 +71,4 @@ }, "minimum-stability": "stable", "prefer-stable": true -} \ No newline at end of file +} diff --git a/composer.lock b/composer.lock index bfe5e2d..4243474 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "d8630be1b4134dfd767005faca33491d", + "content-hash": "ea5a7c49862ceb1f5b573b695143a176", "packages": [ { "name": "bacon/bacon-qr-code", @@ -1552,6 +1552,86 @@ }, "time": "2023-12-19T18:44:48+00:00" }, + { + "name": "laravel/scout", + "version": "v11.1.0", + "source": { + "type": "git", + "url": "https://github.com/laravel/scout.git", + "reference": "9865fca4129d79c271da6a89afb44359e9ee9020" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/laravel/scout/zipball/9865fca4129d79c271da6a89afb44359e9ee9020", + "reference": "9865fca4129d79c271da6a89afb44359e9ee9020", + "shasum": "" + }, + "require": { + "illuminate/bus": "^9.0|^10.0|^11.0|^12.0|^13.0", + "illuminate/contracts": "^9.0|^10.0|^11.0|^12.0|^13.0", + "illuminate/database": "^9.0|^10.0|^11.0|^12.0|^13.0", + "illuminate/http": "^9.0|^10.0|^11.0|^12.0|^13.0", + "illuminate/pagination": "^9.0|^10.0|^11.0|^12.0|^13.0", + "illuminate/queue": "^9.0|^10.0|^11.0|^12.0|^13.0", + "illuminate/support": "^9.0|^10.0|^11.0|^12.0|^13.0", + "php": "^8.0", + "symfony/console": "^6.0|^7.0|^8.0" + }, + "conflict": { + "algolia/algoliasearch-client-php": "<3.2.0|>=5.0.0" + }, + "require-dev": { + "algolia/algoliasearch-client-php": "^3.2|^4.0", + "meilisearch/meilisearch-php": "^1.0", + "mockery/mockery": "^1.0", + "orchestra/testbench": "^7.31|^8.36|^9.15|^10.8|^11.0", + "php-http/guzzle7-adapter": "^1.0", + "phpstan/phpstan": "^1.10", + "typesense/typesense-php": "^4.9.3" + }, + "suggest": { + "algolia/algoliasearch-client-php": "Required to use the Algolia engine (^3.2).", + "meilisearch/meilisearch-php": "Required to use the Meilisearch engine (^1.0).", + "typesense/typesense-php": "Required to use the Typesense engine (^4.9)." + }, + "type": "library", + "extra": { + "laravel": { + "providers": [ + "Laravel\\Scout\\ScoutServiceProvider" + ] + }, + "branch-alias": { + "dev-master": "11.x-dev" + } + }, + "autoload": { + "psr-4": { + "Laravel\\Scout\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Taylor Otwell", + "email": "taylor@laravel.com" + } + ], + "description": "Laravel Scout provides a driver based solution to searching your Eloquent models.", + "keywords": [ + "algolia", + "laravel", + "search" + ], + "support": { + "issues": "https://github.com/laravel/scout/issues", + "source": "https://github.com/laravel/scout" + }, + "time": "2026-03-18T14:50:59+00:00" + }, { "name": "laravel/serializable-closure", "version": "v1.3.7", diff --git a/config/scout.php b/config/scout.php new file mode 100644 index 0000000..36eafda --- /dev/null +++ b/config/scout.php @@ -0,0 +1,210 @@ + env('SCOUT_DRIVER', 'database'), + + /* + |-------------------------------------------------------------------------- + | Index Prefix + |-------------------------------------------------------------------------- + | + | Here you may specify a prefix that will be applied to all search index + | names used by Scout. This prefix may be useful if you have multiple + | "tenants" or applications sharing the same search infrastructure. + | + */ + + 'prefix' => env('SCOUT_PREFIX', ''), + + /* + |-------------------------------------------------------------------------- + | Queue Data Syncing + |-------------------------------------------------------------------------- + | + | This option allows you to control if the operations that sync your data + | with your search engines are queued. When this is set to "true" then + | all automatic data syncing will get queued for better performance. + | + */ + + 'queue' => env('SCOUT_QUEUE', false), + + /* + |-------------------------------------------------------------------------- + | Database Transactions + |-------------------------------------------------------------------------- + | + | This configuration option determines if your data will only be synced + | with your search indexes after every open database transaction has + | been committed, thus preventing any discarded data from syncing. + | + */ + + 'after_commit' => false, + + /* + |-------------------------------------------------------------------------- + | Chunk Sizes + |-------------------------------------------------------------------------- + | + | These options allow you to control the maximum chunk size when you are + | mass importing data into the search engine. This allows you to fine + | tune each of these chunk sizes based on the power of the servers. + | + */ + + 'chunk' => [ + 'searchable' => 500, + 'unsearchable' => 500, + ], + + /* + |-------------------------------------------------------------------------- + | Soft Deletes + |-------------------------------------------------------------------------- + | + | This option allows to control whether to keep soft deleted records in + | the search indexes. Maintaining soft deleted records can be useful + | if your application still needs to search for the records later. + | + */ + + 'soft_delete' => false, + + /* + |-------------------------------------------------------------------------- + | Identify User + |-------------------------------------------------------------------------- + | + | This option allows you to control whether to notify the search engine + | of the user performing the search. This is sometimes useful if the + | engine supports any analytics based on this application's users. + | + | Supported engines: "algolia" + | + */ + + 'identify' => env('SCOUT_IDENTIFY', false), + + /* + |-------------------------------------------------------------------------- + | Algolia Configuration + |-------------------------------------------------------------------------- + | + | Here you may configure your Algolia settings. Algolia is a cloud hosted + | search engine which works great with Scout out of the box. Just plug + | in your application ID and admin API key to get started searching. + | + */ + + 'algolia' => [ + 'id' => env('ALGOLIA_APP_ID', ''), + 'secret' => env('ALGOLIA_SECRET', ''), + 'index-settings' => [ + // 'users' => [ + // 'searchableAttributes' => ['id', 'name', 'email'], + // 'attributesForFaceting'=> ['filterOnly(email)'], + // ], + ], + ], + + /* + |-------------------------------------------------------------------------- + | Meilisearch Configuration + |-------------------------------------------------------------------------- + | + | Here you may configure your Meilisearch settings. Meilisearch is an open + | source search engine with minimal configuration. Below, you can state + | the host and key information for your own Meilisearch installation. + | + | See: https://www.meilisearch.com/docs/learn/configuration/instance_options#all-instance-options + | + */ + + 'meilisearch' => [ + 'host' => env('MEILISEARCH_HOST', 'http://localhost:7700'), + 'key' => env('MEILISEARCH_KEY'), + 'index-settings' => [ + // 'users' => [ + // 'filterableAttributes'=> ['id', 'name', 'email'], + // ], + ], + ], + + /* + |-------------------------------------------------------------------------- + | Typesense Configuration + |-------------------------------------------------------------------------- + | + | Here you may configure your Typesense settings. Typesense is an open + | source search engine using minimal configuration. Below, you will + | state the host, key, and schema configuration for the instance. + | + */ + + 'typesense' => [ + 'client-settings' => [ + 'api_key' => env('TYPESENSE_API_KEY', 'xyz'), + 'nodes' => [ + [ + 'host' => env('TYPESENSE_HOST', 'localhost'), + 'port' => env('TYPESENSE_PORT', '8108'), + 'path' => env('TYPESENSE_PATH', ''), + 'protocol' => env('TYPESENSE_PROTOCOL', 'http'), + ], + ], + 'nearest_node' => [ + 'host' => env('TYPESENSE_HOST', 'localhost'), + 'port' => env('TYPESENSE_PORT', '8108'), + 'path' => env('TYPESENSE_PATH', ''), + 'protocol' => env('TYPESENSE_PROTOCOL', 'http'), + ], + 'connection_timeout_seconds' => env('TYPESENSE_CONNECTION_TIMEOUT_SECONDS', 2), + 'healthcheck_interval_seconds' => env('TYPESENSE_HEALTHCHECK_INTERVAL_SECONDS', 30), + 'num_retries' => env('TYPESENSE_NUM_RETRIES', 3), + 'retry_interval_seconds' => env('TYPESENSE_RETRY_INTERVAL_SECONDS', 1), + ], + // 'max_total_results' => env('TYPESENSE_MAX_TOTAL_RESULTS', 1000), + 'model-settings' => [ + // User::class => [ + // 'collection-schema' => [ + // 'fields' => [ + // [ + // 'name' => 'id', + // 'type' => 'string', + // ], + // [ + // 'name' => 'name', + // 'type' => 'string', + // ], + // [ + // 'name' => 'created_at', + // 'type' => 'int64', + // ], + // ], + // 'default_sorting_field' => 'created_at', + // ], + // 'search-parameters' => [ + // 'query_by' => 'name' + // ], + // ], + ], + 'import_action' => env('TYPESENSE_IMPORT_ACTION', 'upsert'), + ], + +]; diff --git a/resources/views/livewire/clients-table.blade.php b/resources/views/livewire/clients-table.blade.php index 3bb832a..32aeaa0 100644 --- a/resources/views/livewire/clients-table.blade.php +++ b/resources/views/livewire/clients-table.blade.php @@ -88,7 +88,7 @@ class="d-none d-flex position-absolute w-100 h-100 top-0 start-0 align-items-cen @else - {!! $complexesNames[0] !!} + @endif @if (auth()->user()->isCityManager())