diff --git a/app/Classes/RcnApi/Entities/InstructionTranslation.php b/app/Classes/RcnApi/Entities/InstructionTranslation.php index 52017cd..d6b8f32 100644 --- a/app/Classes/RcnApi/Entities/InstructionTranslation.php +++ b/app/Classes/RcnApi/Entities/InstructionTranslation.php @@ -12,7 +12,7 @@ class InstructionTranslation implements \JsonSerializable, Arrayable 'warning', 'anticipated', 'assess_and_plan', - 'mitigate_risks', + 'mitigate_risk', 'prepare_to_respond', 'recover' ]; @@ -47,11 +47,11 @@ class InstructionTranslation implements \JsonSerializable, Arrayable public static function createFromRequest(array $array) { $translation = new self(); - $translation->lang = $array['lang']; - $translation->webUrl = $array['webUrl']; - $translation->title = $array['title']; - $translation->description = $array['description']; - $translation->stages = new Collection($array['stages']); + $translation->lang = $array['lang']; + $translation->webUrl = $array['webUrl']; + $translation->title = $array['title']; + $translation->description = $array['description']; + $translation->stages = new Collection($array['stages']); return $translation; } @@ -60,12 +60,12 @@ public static function createFromResponse(array $array) { $translation = new self(); - $translation->id = $array['id']; - $translation->lang = $array['lang']; - $translation->webUrl = $array['webUrl']; - $translation->title = $array['title']; - $translation->description = $array['description']; - $translation->stages = new Collection($array['stages']); + $translation->id = $array['id']; + $translation->lang = $array['lang']; + $translation->webUrl = $array['webUrl']; + $translation->title = $array['title']; + $translation->description = $array['description']; + $translation->stages = new Collection($array['stages']); $translation->createdAt = new \DateTimeImmutable($array['createdAt']); $translation->published = $array['published']; return $translation; diff --git a/app/Classes/RcnApi/Importer/BulkUploadTemplateExport.php b/app/Classes/RcnApi/Importer/BulkUploadTemplateExport.php index ecc3015..86db556 100644 --- a/app/Classes/RcnApi/Importer/BulkUploadTemplateExport.php +++ b/app/Classes/RcnApi/Importer/BulkUploadTemplateExport.php @@ -23,25 +23,31 @@ class BulkUploadTemplateExport implements FromArray, ShouldAutoSize, WithEvents private $nationalSociety; private $region; private $headings = [ - 'Title', 'Description', 'URL', 'Hazard', 'Urgency Level', 'Safety Message' + 'Title', + 'Description', + 'URL', + 'Hazard', + 'Urgency Level', + 'Safety Message' ]; - private $urgencyLevels = '"Immediate,Warning,Anticipated,Assess and Plan,Mitigate Risks,Prepare to Respond,Recover"'; + private $urgencyLevels = '"Immediate,Warning,Anticipated,Assess and Plan,Mitigate Risk,Prepare to Respond,Recover"'; private $eventTypesDropdown = []; private $data; - public function __construct(string $nationalSociety, string $region,array $data, int $maxSupportingMessages) + public function __construct(string $nationalSociety, string $region, array $data, int $maxSupportingMessages) { $eventTypes = EventType::whereNotIn('code', ['other'])->get()->toArray(); $this->nationalSociety = $nationalSociety; $this->eventTypesDropdown = '"' . implode(',', array_map(function ($event) { - return "{$event['name']}"; - }, $eventTypes)) . '"'; + return "{$event['name']}"; + }, $eventTypes)) . '"'; $this->subnational = $region; $this->data = $data; - if($maxSupportingMessages <= 0) $maxSupportingMessages = 3; - for($i = 0; $i< $maxSupportingMessages; $i++){ + if ($maxSupportingMessages <= 0) + $maxSupportingMessages = 3; + for ($i = 0; $i < $maxSupportingMessages; $i++) { $this->headings[] = 'Supporting Message ' . ($i + 1); } diff --git a/app/Http/Requests/UserListRequest.php b/app/Http/Requests/UserListRequest.php index e5fee77..6782162 100644 --- a/app/Http/Requests/UserListRequest.php +++ b/app/Http/Requests/UserListRequest.php @@ -21,7 +21,8 @@ public function rules() { return [ 'orderBy' => 'in:first_name,last_name,organisation,industry_type,created_at,last_logged_in_at', - 'sort' => 'in:asc,desc' + 'sort' => 'in:asc,desc', + 'filters.search' => 'nullable|string|min:3|max:191' ]; } @@ -36,8 +37,10 @@ public function getUserQuery(): UserQuery $userQuery->setOrderBy($orderBy); $userQuery->setSort($sort); - foreach ($this->get('filters', []) as $column => $value) { - $userQuery->addFilter($column, $value); + $filters = $this->get('filters', $this->get('filters\\', [])); + + foreach ($filters as $column => $value) { + $userQuery->addFilter(rtrim($column, '\\'), $value); } return $userQuery; diff --git a/app/Repositories/Access/UserRepository.php b/app/Repositories/Access/UserRepository.php index d2a57bd..183d956 100644 --- a/app/Repositories/Access/UserRepository.php +++ b/app/Repositories/Access/UserRepository.php @@ -186,6 +186,11 @@ public function queryUsers(UserQuery $userQuery): Builder }); }); + $search = trim($userQuery->getFilters()->get('search', '')); + if (mb_strlen($search) >= 3) { + $this->applySearchFilter($builder, $search); + } + $society = $userQuery->getFilters()->only(['society'])->first(); if ($society) { $builder->whereHas('organisations', function ($query) use ($society) { @@ -214,12 +219,25 @@ public function queryUsers(UserQuery $userQuery): Builder } if (in_array($userQuery->getOrderBy(), ['first_name', 'last_name', 'organisation', 'industry_type'])) { - $builder->orderByRaw('(SELECT ' . $userQuery->getOrderBy() . ' FROM user_profiles WHERE user_profiles.user_id = users.id) ' . $userQuery->getSort()); + $builder->orderByRaw('(SELECT ' . $userQuery->getOrderBy() . ' FROM user_profiles WHERE user_profiles.user_id = users.id AND user_profiles.deleted_at IS NULL ORDER BY user_profiles.id DESC LIMIT 1) ' . $userQuery->getSort()); } return $builder; } + private function applySearchFilter(Builder $builder, string $search) + { + $profileMatch = 'MATCH(user_profiles.first_name, user_profiles.last_name) AGAINST (? IN NATURAL LANGUAGE MODE)'; + $emailMatch = 'MATCH(users.email) AGAINST (? IN NATURAL LANGUAGE MODE)'; + $profileScore = "(SELECT {$profileMatch} FROM user_profiles WHERE user_profiles.user_id = users.id AND user_profiles.deleted_at IS NULL ORDER BY user_profiles.id DESC LIMIT 1)"; + $score = "({$emailMatch} + COALESCE({$profileScore}, 0))"; + + $builder->select('users.*') + ->selectRaw("{$score} as search_score", [$search, $search]) + ->whereRaw("({$emailMatch} > 0 OR EXISTS (SELECT 1 FROM user_profiles WHERE user_profiles.user_id = users.id AND user_profiles.deleted_at IS NULL AND {$profileMatch} > 0))", [$search, $search]) + ->orderBy('search_score', 'desc'); + } + public function deactivate(User $user) { $user->activated = false; diff --git a/database/migrations/2026_05_29_000000_add_fulltext_indexes_for_user_search.php b/database/migrations/2026_05_29_000000_add_fulltext_indexes_for_user_search.php new file mode 100644 index 0000000..1780884 --- /dev/null +++ b/database/migrations/2026_05_29_000000_add_fulltext_indexes_for_user_search.php @@ -0,0 +1,51 @@ +indexExists('users', 'users_email_fulltext_search')) { + DB::statement('ALTER TABLE users ADD FULLTEXT users_email_fulltext_search(email)'); + } + + $this->dropIndexIfExists('user_profiles', 'user_profiles_search_fulltext'); + DB::statement('ALTER TABLE user_profiles ADD FULLTEXT user_profiles_search_fulltext(first_name, last_name)'); + } + + + public function down() + { + if (env('DB_CONNECTION') !== 'mysql') { + return; + } + + $this->dropIndexIfExists('users', 'users_email_fulltext_search'); + $this->dropIndexIfExists('user_profiles', 'user_profiles_search_fulltext'); + } + + private function dropIndexIfExists(string $table, string $index) + { + if ($this->indexExists($table, $index)) { + DB::statement("ALTER TABLE {$table} DROP INDEX {$index}"); + } + } + + private function indexExists(string $table, string $index): bool + { + return (bool) DB::select( + DB::raw( + "SHOW KEYS + FROM {$table} + WHERE Key_name='{$index}'" + ) + ); + } +} diff --git a/resources/assets/js/app.js b/resources/assets/js/app.js index 5207562..397228e 100644 --- a/resources/assets/js/app.js +++ b/resources/assets/js/app.js @@ -42,8 +42,11 @@ Vue.use(VueMoment) Vue.use(TscGTAG) -Vue.use(GoogleAuth, { client_id: window.config.google.client_id, scopes: 'profile email openid' }) -Vue.googleAuth().load() +const googleClientId = window.config?.google?.client_id +if (googleClientId) { + Vue.use(GoogleAuth, { client_id: googleClientId, scopes: 'profile email openid' }) + Vue.googleAuth().load() +} Vue.config.productionTip = false diff --git a/resources/assets/js/pages/content/editWhatnow.vue b/resources/assets/js/pages/content/editWhatnow.vue index 5b70504..2a023a7 100644 --- a/resources/assets/js/pages/content/editWhatnow.vue +++ b/resources/assets/js/pages/content/editWhatnow.vue @@ -343,7 +343,7 @@ export default { 'warning', 'anticipated', 'assess_and_plan', - 'mitigate_risks', + 'mitigate_risk', 'prepare_to_respond', 'recover' ], @@ -358,7 +358,7 @@ export default { { value: 'disaster_risk_reduction', text: this.$t('content.edit_whatnow.disaster_risk_reduction'), - stages: ['assess_and_plan', 'mitigate_risks', 'prepare_to_respond'], + stages: ['assess_and_plan', 'mitigate_risk', 'prepare_to_respond'], description: this.$t('content.edit_whatnow.disaster_risk_reduction_description') }, { diff --git a/resources/assets/js/pages/content/selectSociety.vue b/resources/assets/js/pages/content/selectSociety.vue index c3d1b77..edecbbe 100644 --- a/resources/assets/js/pages/content/selectSociety.vue +++ b/resources/assets/js/pages/content/selectSociety.vue @@ -4,7 +4,7 @@ v-model="selectedSoc" class="w-100 v-select-custom" :options="listOfSocieties" - label="name" :disabled="listOfSocieties.length === 0" + label="name" :disabled="disabled || listOfSocieties.length === 0" :placeholder="$t('content.whatnow.no_soc')">