feat: Phase 2 - Laravel Reverb + real-time notifications

- Install laravel/reverb + laravel-echo + pusher-js
- Configure broadcasting to reverb in .env.example + .env
- Add Echo + Pusher client in resources/js/app.js
- Update all 14 notifications to implement ShouldBroadcast + broadcastOn()
- Update NotificationBell component to listen for real-time events
- Add Livewire.emit in notification-bell blade for JS-to-Livewire bridge

Tests: 101 passing (319 assertions)
This commit is contained in:
Javier Braña
2026-08-31 12:16:56 +02:00
parent 78d1dbf45a
commit 9222eefdf9
26 changed files with 1192 additions and 44 deletions
@@ -4,9 +4,10 @@ namespace App\Notifications;
use App\Models\Feature;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Broadcasting\ShouldBroadcast;
use Illuminate\Notifications\Notification;
class FeatureCompletedNotification extends Notification
class FeatureCompletedNotification extends Notification implements ShouldBroadcast
{
use Queueable;
@@ -14,7 +15,7 @@ class FeatureCompletedNotification extends Notification
public function via($notifiable): array
{
return ['database'];
return ['database', 'broadcast'];
}
public function toArray($notifiable): array
@@ -28,4 +29,14 @@ class FeatureCompletedNotification extends Notification
'message' => "Elemento '{$this->feature->name}' marcado como completado",
];
}
}
public function broadcastOn(): array
{
$projectId = $this->feature->layer?->phase?->project_id;
if (! $projectId) {
return [];
}
$userIds = \App\Models\Project::find($projectId)?->users->pluck('id')->toArray() ?? [];
return array_map(fn ($id) => new \Illuminate\Broadcasting\PrivateChannel("user.{$id}"), $userIds);
}
}
@@ -4,9 +4,10 @@ namespace App\Notifications;
use App\Models\Inspection;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Broadcasting\ShouldBroadcast;
use Illuminate\Notifications\Notification;
class InspectionCompletedNotification extends Notification
class InspectionCompletedNotification extends Notification implements ShouldBroadcast
{
use Queueable;
@@ -14,7 +15,7 @@ class InspectionCompletedNotification extends Notification
public function via($notifiable): array
{
return ['database'];
return ['database', 'broadcast'];
}
public function toArray($notifiable): array
@@ -29,4 +30,10 @@ class InspectionCompletedNotification extends Notification
'message' => "Inspección completada en '{$this->inspection->feature?->name}': ".($this->inspection->result ?? 'sin resultado'),
];
}
}
public function broadcastOn(): array
{
$userIds = $this->inspection->project->users->pluck('id')->toArray();
return array_map(fn ($id) => new \Illuminate\Broadcasting\PrivateChannel("user.{$id}"), $userIds);
}
}
@@ -4,9 +4,10 @@ namespace App\Notifications;
use App\Models\Inspection;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Broadcasting\ShouldBroadcast;
use Illuminate\Notifications\Notification;
class InspectionDeletedNotification extends Notification
class InspectionDeletedNotification extends Notification implements ShouldBroadcast
{
use Queueable;
@@ -14,7 +15,7 @@ class InspectionDeletedNotification extends Notification
public function via($notifiable): array
{
return ['database'];
return ['database', 'broadcast'];
}
public function toArray($notifiable): array
@@ -29,4 +30,10 @@ class InspectionDeletedNotification extends Notification
'message' => "Inspección eliminada en '{$this->inspection->feature?->name}' por {$this->deletedBy}",
];
}
}
public function broadcastOn(): array
{
$userIds = $this->inspection->project->users->pluck('id')->toArray();
return array_map(fn ($id) => new \Illuminate\Broadcasting\PrivateChannel("user.{$id}"), $userIds);
}
}
@@ -4,9 +4,10 @@ namespace App\Notifications;
use App\Models\Inspection;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Broadcasting\ShouldBroadcast;
use Illuminate\Notifications\Notification;
class InspectionUpdatedNotification extends Notification
class InspectionUpdatedNotification extends Notification implements ShouldBroadcast
{
use Queueable;
@@ -14,7 +15,7 @@ class InspectionUpdatedNotification extends Notification
public function via($notifiable): array
{
return ['database'];
return ['database', 'broadcast'];
}
public function toArray($notifiable): array
@@ -30,4 +31,10 @@ class InspectionUpdatedNotification extends Notification
'message' => "Inspección actualizada en '{$this->inspection->feature?->name}'",
];
}
}
public function broadcastOn(): array
{
$userIds = $this->inspection->project->users->pluck('id')->toArray();
return array_map(fn ($id) => new \Illuminate\Broadcasting\PrivateChannel("user.{$id}"), $userIds);
}
}
@@ -4,9 +4,10 @@ namespace App\Notifications;
use App\Models\Issue;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Broadcasting\ShouldBroadcast;
use Illuminate\Notifications\Notification;
class IssueAssignedNotification extends Notification
class IssueAssignedNotification extends Notification implements ShouldBroadcast
{
use Queueable;
@@ -14,7 +15,7 @@ class IssueAssignedNotification extends Notification
public function via($notifiable): array
{
return ['database'];
return ['database', 'broadcast'];
}
public function toArray($notifiable): array
@@ -27,4 +28,9 @@ class IssueAssignedNotification extends Notification
'message' => "Se te ha asignado la incidencia '{$this->issue->title}'",
];
}
}
public function broadcastOn(): array
{
return [new \Illuminate\Broadcasting\PrivateChannel('user.'.$this->issue->assigned_to)];
}
}
@@ -4,10 +4,11 @@ namespace App\Notifications;
use App\Models\IssueComment;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Broadcasting\ShouldBroadcast;
use Illuminate\Notifications\Notification;
use Illuminate\Support\Str;
class IssueCommentedNotification extends Notification
class IssueCommentedNotification extends Notification implements ShouldBroadcast
{
use Queueable;
@@ -15,7 +16,7 @@ class IssueCommentedNotification extends Notification
public function via($notifiable): array
{
return ['database'];
return ['database', 'broadcast'];
}
public function toArray($notifiable): array
@@ -30,4 +31,13 @@ class IssueCommentedNotification extends Notification
'message' => "{$this->comment->user?->name} comentó en '{$issue?->title}': ".Str::limit($this->comment->body, 60),
];
}
}
public function broadcastOn(): array
{
$issue = $this->comment->issue;
$userIds = [];
if ($issue->assigned_to) $userIds[] = $issue->assigned_to;
if ($issue->reported_by) $userIds[] = $issue->reported_by;
return array_map(fn ($id) => new \Illuminate\Broadcasting\PrivateChannel("user.{$id}"), array_unique($userIds));
}
}
@@ -4,9 +4,10 @@ namespace App\Notifications;
use App\Models\Issue;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Broadcasting\ShouldBroadcast;
use Illuminate\Notifications\Notification;
class IssueReportedNotification extends Notification
class IssueReportedNotification extends Notification implements ShouldBroadcast
{
use Queueable;
@@ -14,7 +15,7 @@ class IssueReportedNotification extends Notification
public function via($notifiable): array
{
return ['database'];
return ['database', 'broadcast'];
}
public function toArray($notifiable): array
@@ -28,4 +29,10 @@ class IssueReportedNotification extends Notification
'message' => "Nuevo issue '{$this->issue->title}' (prioridad: {$this->issue->priority})",
];
}
}
public function broadcastOn(): array
{
$userIds = $this->issue->project->users->pluck('id')->toArray();
return array_map(fn ($id) => new \Illuminate\Broadcasting\PrivateChannel("user.{$id}"), $userIds);
}
}
@@ -4,9 +4,10 @@ namespace App\Notifications;
use App\Models\Issue;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Broadcasting\ShouldBroadcast;
use Illuminate\Notifications\Notification;
class IssueStatusChangedNotification extends Notification
class IssueStatusChangedNotification extends Notification implements ShouldBroadcast
{
use Queueable;
@@ -14,7 +15,7 @@ class IssueStatusChangedNotification extends Notification
public function via($notifiable): array
{
return ['database'];
return ['database', 'broadcast'];
}
public function toArray($notifiable): array
@@ -34,4 +35,12 @@ class IssueStatusChangedNotification extends Notification
'message' => "La incidencia '{$this->issue->title}' ha sido {$label}",
];
}
}
public function broadcastOn(): array
{
$userIds = [];
if ($this->issue->assigned_to) $userIds[] = $this->issue->assigned_to;
if ($this->issue->reported_by) $userIds[] = $this->issue->reported_by;
return array_map(fn ($id) => new \Illuminate\Broadcasting\PrivateChannel("user.{$id}"), array_unique($userIds));
}
}
@@ -4,9 +4,10 @@ namespace App\Notifications;
use App\Models\IssueTask;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Broadcasting\ShouldBroadcast;
use Illuminate\Notifications\Notification;
class IssueTaskAssignedNotification extends Notification
class IssueTaskAssignedNotification extends Notification implements ShouldBroadcast
{
use Queueable;
@@ -14,7 +15,7 @@ class IssueTaskAssignedNotification extends Notification
public function via($notifiable): array
{
return ['database'];
return ['database', 'broadcast'];
}
public function toArray($notifiable): array
@@ -28,4 +29,9 @@ class IssueTaskAssignedNotification extends Notification
'message' => "Se te ha asignado la tarea '{$this->task->title}'",
];
}
}
public function broadcastOn(): array
{
return [new \Illuminate\Broadcasting\PrivateChannel('user.'.$this->task->assigned_to)];
}
}
@@ -4,9 +4,10 @@ namespace App\Notifications;
use App\Models\IssueTask;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Broadcasting\ShouldBroadcast;
use Illuminate\Notifications\Notification;
class IssueTaskOverdueNotification extends Notification
class IssueTaskOverdueNotification extends Notification implements ShouldBroadcast
{
use Queueable;
@@ -14,7 +15,7 @@ class IssueTaskOverdueNotification extends Notification
public function via($notifiable): array
{
return ['database'];
return ['database', 'broadcast'];
}
public function toArray($notifiable): array
@@ -28,4 +29,9 @@ class IssueTaskOverdueNotification extends Notification
'message' => "Tarea vencida: '{$this->task->title}' (venció el {$this->task->due_date?->format('d/m/Y')})",
];
}
}
public function broadcastOn(): array
{
return [new \Illuminate\Broadcasting\PrivateChannel('user.'.$this->task->assigned_to)];
}
}
@@ -4,9 +4,10 @@ namespace App\Notifications;
use App\Models\Task;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Broadcasting\ShouldBroadcast;
use Illuminate\Notifications\Notification;
class TaskAssignedNotification extends Notification
class TaskAssignedNotification extends Notification implements ShouldBroadcast
{
use Queueable;
@@ -14,7 +15,7 @@ class TaskAssignedNotification extends Notification
public function via($notifiable): array
{
return ['database'];
return ['database', 'broadcast'];
}
public function toArray($notifiable): array
@@ -27,4 +28,9 @@ class TaskAssignedNotification extends Notification
'message' => "Se te ha asignado la tarea '{$this->task->title}'",
];
}
}
public function broadcastOn(): array
{
return [new \Illuminate\Broadcasting\PrivateChannel('user.'.$this->task->assigned_to)];
}
}
+12 -3
View File
@@ -5,9 +5,10 @@ namespace App\Notifications;
use App\Models\Comment;
use App\Models\Task;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Broadcasting\ShouldBroadcast;
use Illuminate\Notifications\Notification;
class TaskCommentNotification extends Notification
class TaskCommentNotification extends Notification implements ShouldBroadcast
{
use Queueable;
@@ -15,7 +16,7 @@ class TaskCommentNotification extends Notification
public function via($notifiable): array
{
return ['database'];
return ['database', 'broadcast'];
}
public function toArray($notifiable): array
@@ -28,4 +29,12 @@ class TaskCommentNotification extends Notification
'message' => "Nuevo comentario en '{$this->task->title}' por {$this->comment->user->name}: ".substr($this->comment->body, 0, 100),
];
}
}
public function broadcastOn(): array
{
$userIds = [];
if ($this->task->assigned_to) $userIds[] = $this->task->assigned_to;
if ($this->task->created_by) $userIds[] = $this->task->created_by;
return array_map(fn ($id) => new \Illuminate\Broadcasting\PrivateChannel("user.{$id}"), array_unique($userIds));
}
}
@@ -4,9 +4,10 @@ namespace App\Notifications;
use App\Models\Task;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Broadcasting\ShouldBroadcast;
use Illuminate\Notifications\Notification;
class TaskOverdueNotification extends Notification
class TaskOverdueNotification extends Notification implements ShouldBroadcast
{
use Queueable;
@@ -14,7 +15,7 @@ class TaskOverdueNotification extends Notification
public function via($notifiable): array
{
return ['database'];
return ['database', 'broadcast'];
}
public function toArray($notifiable): array
@@ -27,4 +28,9 @@ class TaskOverdueNotification extends Notification
'message' => "Tarea vencida: '{$this->task->title}' (venció el {$this->task->due_date?->format('d/m/Y')})",
];
}
}
public function broadcastOn(): array
{
return [new \Illuminate\Broadcasting\PrivateChannel('user.'.$this->task->assigned_to)];
}
}
@@ -4,9 +4,10 @@ namespace App\Notifications;
use App\Models\Task;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Broadcasting\ShouldBroadcast;
use Illuminate\Notifications\Notification;
class TaskStatusChangedNotification extends Notification
class TaskStatusChangedNotification extends Notification implements ShouldBroadcast
{
use Queueable;
@@ -14,7 +15,7 @@ class TaskStatusChangedNotification extends Notification
public function via($notifiable): array
{
return ['database'];
return ['database', 'broadcast'];
}
public function toArray($notifiable): array
@@ -42,4 +43,12 @@ class TaskStatusChangedNotification extends Notification
'message' => "La tarea '{$this->task->title}' cambió de {$oldLabel} a {$label}",
];
}
}
public function broadcastOn(): array
{
$userIds = [];
if ($this->task->assigned_to) $userIds[] = $this->task->assigned_to;
if ($this->task->created_by) $userIds[] = $this->task->created_by;
return array_map(fn ($id) => new \Illuminate\Broadcasting\PrivateChannel("user.{$id}"), array_unique($userIds));
}
}