feat(issues): notificaciones, plantillas de checklist, alertas de vencimiento y reporte desde el mapa

- Notificaciones (DB): asignación de incidencia (IssueAssigned), asignación de tarea
  (IssueTaskAssigned), comentario (IssueCommented) y cambio de estado
  (IssueStatusChanged) a reporter+asignado excluyendo al actor.
- Plantillas de checklist: tabla issue_checklist_templates + modelo, gestor CRUD
  (IssueChecklistManager, ruta projects.issues.checklists) y "Aplicar plantilla" en
  el detalle (alta masiva de tareas).
- Alertas de vencimiento: columna overdue_notified_at + scope overdue, comando
  issues:notify-overdue (programado a diario) que avisa al asignado una sola vez;
  badge "vencidas" en la tabla y resaltado por tarea en el detalle.
- Reporte desde el mapa: botón "Incidencia" en el panel del feature seleccionado →
  formulario con feature pre-vinculado (IssueForm lee ?feature=).

Tests: IssuesEnhancementsTest (7). Suite 57 passing (solo 2 pre-existentes sqlite).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-06-18 12:51:41 +02:00
parent 3f240e5277
commit 8c774d075d
22 changed files with 818 additions and 15 deletions
@@ -0,0 +1,30 @@
<?php
namespace App\Notifications;
use App\Models\Issue;
use Illuminate\Bus\Queueable;
use Illuminate\Notifications\Notification;
class IssueAssignedNotification extends Notification
{
use Queueable;
public function __construct(public Issue $issue) {}
public function via($notifiable): array
{
return ['database'];
}
public function toArray($notifiable): array
{
return [
'type' => 'issue_assigned',
'issue_id' => $this->issue->id,
'project_id' => $this->issue->project_id,
'priority' => $this->issue->priority,
'message' => "Se te ha asignado la incidencia '{$this->issue->title}'",
];
}
}
@@ -0,0 +1,33 @@
<?php
namespace App\Notifications;
use App\Models\IssueComment;
use Illuminate\Bus\Queueable;
use Illuminate\Notifications\Notification;
use Illuminate\Support\Str;
class IssueCommentedNotification extends Notification
{
use Queueable;
public function __construct(public IssueComment $comment) {}
public function via($notifiable): array
{
return ['database'];
}
public function toArray($notifiable): array
{
$issue = $this->comment->issue;
return [
'type' => 'issue_commented',
'issue_id' => $this->comment->issue_id,
'project_id' => $issue?->project_id,
'author' => $this->comment->user?->name,
'message' => "{$this->comment->user?->name} comentó en '{$issue?->title}': " . Str::limit($this->comment->body, 60),
];
}
}
@@ -0,0 +1,37 @@
<?php
namespace App\Notifications;
use App\Models\Issue;
use Illuminate\Bus\Queueable;
use Illuminate\Notifications\Notification;
class IssueStatusChangedNotification extends Notification
{
use Queueable;
public function __construct(public Issue $issue, public string $status) {}
public function via($notifiable): array
{
return ['database'];
}
public function toArray($notifiable): array
{
$label = [
'open' => 'reabierta',
'in_review' => 'enviada a revisión',
'resolved' => 'resuelta',
'closed' => 'cerrada',
][$this->status] ?? $this->status;
return [
'type' => 'issue_status_changed',
'issue_id' => $this->issue->id,
'project_id' => $this->issue->project_id,
'status' => $this->status,
'message' => "La incidencia '{$this->issue->title}' ha sido {$label}",
];
}
}
@@ -0,0 +1,31 @@
<?php
namespace App\Notifications;
use App\Models\IssueTask;
use Illuminate\Bus\Queueable;
use Illuminate\Notifications\Notification;
class IssueTaskAssignedNotification extends Notification
{
use Queueable;
public function __construct(public IssueTask $task) {}
public function via($notifiable): array
{
return ['database'];
}
public function toArray($notifiable): array
{
return [
'type' => 'issue_task_assigned',
'issue_id' => $this->task->issue_id,
'task_id' => $this->task->id,
'project_id' => $this->task->issue?->project_id,
'due_date' => $this->task->due_date?->toDateString(),
'message' => "Se te ha asignado la tarea '{$this->task->title}'",
];
}
}
@@ -0,0 +1,31 @@
<?php
namespace App\Notifications;
use App\Models\IssueTask;
use Illuminate\Bus\Queueable;
use Illuminate\Notifications\Notification;
class IssueTaskOverdueNotification extends Notification
{
use Queueable;
public function __construct(public IssueTask $task) {}
public function via($notifiable): array
{
return ['database'];
}
public function toArray($notifiable): array
{
return [
'type' => 'issue_task_overdue',
'issue_id' => $this->task->issue_id,
'task_id' => $this->task->id,
'project_id' => $this->task->issue?->project_id,
'due_date' => $this->task->due_date?->toDateString(),
'message' => "Tarea vencida: '{$this->task->title}' (venció el {$this->task->due_date?->format('d/m/Y')})",
];
}
}