feat: Phase 5.2 - Global Search with Laravel Scout
- 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:
@@ -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');
|
||||
}
|
||||
}
|
||||
+21
-1
@@ -7,10 +7,11 @@ use Carbon\Carbon;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\MorphMany;
|
||||
use Illuminate\Database\Eloquent\SoftDeletes;
|
||||
use Laravel\Scout\Searchable;
|
||||
|
||||
class Feature extends Model
|
||||
{
|
||||
use LogsActivity, SoftDeletes;
|
||||
use LogsActivity, SoftDeletes, Searchable;
|
||||
|
||||
const STATUSES = ['planned', 'started', 'in_progress', 'completed', 'verified'];
|
||||
|
||||
@@ -156,4 +157,23 @@ class Feature extends Model
|
||||
|
||||
return $spi >= 0.95;
|
||||
}
|
||||
|
||||
public function toSearchableArray(): array
|
||||
{
|
||||
return [
|
||||
'id' => $this->id,
|
||||
'name' => $this->name,
|
||||
'description' => $this->description,
|
||||
'project_id' => $this->layer->phase->project_id ?? null,
|
||||
'phase_id' => $this->layer->phase_id ?? null,
|
||||
'layer_id' => $this->layer_id ?? null,
|
||||
'status' => $this->status,
|
||||
'progress' => $this->progress,
|
||||
];
|
||||
}
|
||||
|
||||
public function getScoutKey(): string
|
||||
{
|
||||
return (string) $this->id;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,10 +5,11 @@ namespace App\Models;
|
||||
use App\Traits\LogsActivity;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\SoftDeletes;
|
||||
use Laravel\Scout\Searchable;
|
||||
|
||||
class Inspection extends Model
|
||||
{
|
||||
use LogsActivity, SoftDeletes;
|
||||
use LogsActivity, SoftDeletes, Searchable;
|
||||
|
||||
protected static function booted(): void
|
||||
{
|
||||
@@ -87,4 +88,25 @@ class Inspection extends Model
|
||||
{
|
||||
return $q->where('status', 'rejected');
|
||||
}
|
||||
|
||||
public function toSearchableArray(): array
|
||||
{
|
||||
return [
|
||||
'id' => $this->id,
|
||||
'project_id' => $this->project_id,
|
||||
'layer_id' => $this->layer_id,
|
||||
'feature_id' => $this->feature_id,
|
||||
'template_id' => $this->template_id,
|
||||
'user_id' => $this->user_id,
|
||||
'inspector_user_id' => $this->inspector_user_id,
|
||||
'status' => $this->status,
|
||||
'result' => $this->result,
|
||||
'notes' => $this->notes,
|
||||
];
|
||||
}
|
||||
|
||||
public function getScoutKey(): string
|
||||
{
|
||||
return (string) $this->id;
|
||||
}
|
||||
}
|
||||
|
||||
+24
-1
@@ -5,10 +5,11 @@ namespace App\Models;
|
||||
use App\Traits\LogsActivity;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\SoftDeletes;
|
||||
use Laravel\Scout\Searchable;
|
||||
|
||||
class Issue extends Model
|
||||
{
|
||||
use LogsActivity, SoftDeletes;
|
||||
use LogsActivity, SoftDeletes, Searchable;
|
||||
|
||||
const STATUSES = ['open', 'in_review', 'resolved', 'closed'];
|
||||
|
||||
@@ -141,4 +142,26 @@ class Issue extends Model
|
||||
default => '#6b7280',
|
||||
};
|
||||
}
|
||||
|
||||
public function toSearchableArray(): array
|
||||
{
|
||||
return [
|
||||
'id' => $this->id,
|
||||
'title' => $this->title,
|
||||
'description' => $this->description,
|
||||
'project_id' => $this->project_id,
|
||||
'feature_id' => $this->feature_id,
|
||||
'inspection_id' => $this->inspection_id,
|
||||
'status' => $this->status,
|
||||
'priority' => $this->priority,
|
||||
'type' => $this->type,
|
||||
'reported_by' => $this->reported_by,
|
||||
'assigned_to' => $this->assigned_to,
|
||||
];
|
||||
}
|
||||
|
||||
public function getScoutKey(): string
|
||||
{
|
||||
return (string) $this->id;
|
||||
}
|
||||
}
|
||||
|
||||
+25
-1
@@ -11,10 +11,11 @@ use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
use Illuminate\Database\Eloquent\Relations\HasMany;
|
||||
use Illuminate\Database\Eloquent\Relations\MorphMany;
|
||||
use Illuminate\Database\Eloquent\SoftDeletes;
|
||||
use Laravel\Scout\Searchable;
|
||||
|
||||
class Task extends Model
|
||||
{
|
||||
use HasFactory, LogsActivity, SoftDeletes;
|
||||
use HasFactory, LogsActivity, SoftDeletes, Searchable;
|
||||
|
||||
protected $fillable = [
|
||||
'project_id',
|
||||
@@ -328,4 +329,27 @@ class Task extends Model
|
||||
'critical' => 'Crítica',
|
||||
];
|
||||
}
|
||||
|
||||
public function toSearchableArray(): array
|
||||
{
|
||||
return [
|
||||
'id' => $this->id,
|
||||
'title' => $this->title,
|
||||
'description' => $this->description,
|
||||
'project_id' => $this->project_id,
|
||||
'phase_id' => $this->phase_id,
|
||||
'parent_task_id' => $this->parent_task_id,
|
||||
'status' => $this->status,
|
||||
'priority' => $this->priority,
|
||||
'assigned_to' => $this->assigned_to,
|
||||
'due_date' => $this->due_date?->toDateString(),
|
||||
'start_date' => $this->start_date?->toDateString(),
|
||||
'completed_at' => $this->completed_at?->toDateTimeString(),
|
||||
];
|
||||
}
|
||||
|
||||
public function getScoutKey(): string
|
||||
{
|
||||
return (string) $this->id;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user