feat: Phase 5.2 - Global Search with Laravel Scout
Code Style & Quality / Laravel Pint (push) Waiting to run
Code Style & Quality / PHPStan Static Analysis (push) Waiting to run
Code Style & Quality / Test Suite (push) Waiting to run
Code Style & Quality / Browser Tests (Dusk) (push) Waiting to run

- Install laravel/scout with database driver
- Add Searchable trait to Feature, Issue, Task, Inspection models
- Create GlobalSearch Livewire component with Blade view
- Configure scout.php for database driver + queued syncing
- Add SCOUT_DRIVER=database to .env.example

Tests: 101 passing (319 assertions)
This commit is contained in:
Javier Braña
2026-09-01 13:49:16 +02:00
parent c50a1a2080
commit 55b1a9276a
10 changed files with 686 additions and 5 deletions
+138
View File
@@ -0,0 +1,138 @@
<?php
namespace App\Livewire\Common;
use App\Models\Feature;
use App\Models\Issue;
use App\Models\Inspection;
use App\Models\Project;
use App\Models\Task;
use Illuminate\Support\Facades\Auth;
use Livewire\Attributes\On;
use Livewire\Component;
class GlobalSearch extends Component
{
public string $query = '';
public array $results = [];
public bool $showResults = false;
public int $limit = 5;
public function updatedQuery(): void
{
if (strlen($this->query) < 2) {
$this->results = [];
$this->showResults = false;
return;
}
$this->search();
$this->showResults = true;
}
public function search(): void
{
$q = $this->query;
$projectIds = Auth::user()->projects()->pluck('id');
$features = Feature::search($q)
->where('project_id', $projectIds)
->take($this->limit)
->get()
->map(function ($feature) {
return [
'type' => 'feature',
'type_label' => 'Elemento',
'id' => $feature->id,
'title' => $feature->name,
'subtitle' => $feature->layer?->phase?->name ?? 'Sin fase',
'url' => route('projects.map', $feature->layer?->phase?->project_id).'?feature='.$feature->id,
'icon' => 'square-2-stack',
'color' => 'blue',
];
});
$issues = Issue::search($q)
->where('project_id', $projectIds)
->take($this->limit)
->get()
->map(function ($issue) {
return [
'type' => 'issue',
'type_label' => 'Incidencia',
'id' => $issue->id,
'title' => $issue->title,
'subtitle' => $issue->project?->name ?? 'Sin proyecto',
'url' => route('projects.issues.show', [$issue->project_id, $issue->id]),
'icon' => 'exclamation-triangle',
'color' => 'red',
];
});
$tasks = Task::search($q)
->where('project_id', $projectIds)
->take($this->limit)
->get()
->map(function ($task) {
return [
'type' => 'task',
'type_label' => 'Tarea',
'id' => $task->id,
'title' => $task->title,
'subtitle' => $task->project?->name ?? 'Sin proyecto',
'url' => route('projects.tasks.show', [$task->project_id, $task->id]),
'icon' => 'clipboard-document-check',
'color' => 'green',
];
});
$inspections = Inspection::search($q)
->where('project_id', $projectIds)
->take($this->limit)
->get()
->map(function ($inspection) {
return [
'type' => 'inspection',
'type_label' => 'Inspección',
'id' => $inspection->id,
'title' => $inspection->feature?->name ?? 'Inspección',
'subtitle' => $inspection->template?->name ?? 'Sin template',
'url' => route('projects.map', $inspection->project_id).'?inspection='.$inspection->id,
'icon' => 'magnifying-glass',
'color' => 'purple',
];
});
$this->results = $features
->concat($issues)
->concat($tasks)
->concat($inspections)
->sortBy(fn ($r) => $r['type'])
->take($this->limit * 4)
->values()
->toArray();
}
#[On('close-search-results')]
public function hideResults(): void
{
$this->showResults = false;
}
public function clearSearch(): void
{
$this->query = '';
$this->results = [];
$this->showResults = false;
}
public function render()
{
return view('livewire.common.global-search');
}
}