feat(tasks): complete task management system with Kanban, Calendar, notifications

- Add Task model with subtasks, priorities, status transitions, dates, hours
- Add Comment polymorphic model for tasks/issues/projects
- Livewire components: TaskManager (list+filters), TaskForm (modal), TaskDetail, TaskKanban (drag&drop), TaskCalendar (FullCalendar)
- TaskPolicy with permissions (view/create/edit/delete/assign/manage all)
- Notifications: assigned, status change, overdue, comment added
- Daily overdue notification job scheduled
- Dashboard widget unifies IssueTask + Task
- i18n: en/es/fr/ru (393 keys each)
- Routes, navigation, offline sync support
- 101 tests passing, Pint compliant on new files
This commit is contained in:
Javier Braña
2026-08-03 13:44:10 +02:00
parent a65d4b12f2
commit ba614bddbc
62 changed files with 5878 additions and 382 deletions
@@ -0,0 +1,32 @@
<?php
namespace App\Notifications;
use Illuminate\Bus\Queueable;
use Illuminate\Notifications\Notification;
use App\Models\Inspection;
class InspectionDeletedNotification extends Notification
{
use Queueable;
public function __construct(public Inspection $inspection, public string $deletedBy) {}
public function via($notifiable): array
{
return ['database'];
}
public function toArray($notifiable): array
{
return [
'type' => 'inspection_deleted',
'inspection_id' => $this->inspection->id,
'project_id' => $this->inspection->project_id,
'feature_name' => $this->inspection->feature?->name ?? '—',
'template_name' => $this->inspection->template?->name ?? '—',
'deleted_by' => $this->deletedBy,
'message' => "Inspección eliminada en '{$this->inspection->feature?->name}' por {$this->deletedBy}",
];
}
}
@@ -0,0 +1,33 @@
<?php
namespace App\Notifications;
use Illuminate\Bus\Queueable;
use Illuminate\Notifications\Notification;
use App\Models\Inspection;
class InspectionUpdatedNotification extends Notification
{
use Queueable;
public function __construct(public Inspection $inspection, public array $changes = []) {}
public function via($notifiable): array
{
return ['database'];
}
public function toArray($notifiable): array
{
return [
'type' => 'inspection_updated',
'inspection_id' => $this->inspection->id,
'project_id' => $this->inspection->project_id,
'feature_name' => $this->inspection->feature?->name ?? '—',
'template_name' => $this->inspection->template?->name ?? '—',
'result' => $this->inspection->result,
'changes' => $this->changes,
'message' => "Inspección actualizada en '{$this->inspection->feature?->name}'",
];
}
}
@@ -0,0 +1,30 @@
<?php
namespace App\Notifications;
use App\Models\Task;
use Illuminate\Bus\Queueable;
use Illuminate\Notifications\Notification;
class TaskAssignedNotification extends Notification
{
use Queueable;
public function __construct(public Task $task) {}
public function via($notifiable): array
{
return ['database'];
}
public function toArray($notifiable): array
{
return [
'type' => 'task_assigned',
'task_id' => $this->task->id,
'project_id' => $this->task->project_id,
'priority' => $this->task->priority,
'message' => "Se te ha asignado la tarea '{$this->task->title}'",
];
}
}
@@ -0,0 +1,31 @@
<?php
namespace App\Notifications;
use App\Models\Comment;
use App\Models\Task;
use Illuminate\Bus\Queueable;
use Illuminate\Notifications\Notification;
class TaskCommentNotification extends Notification
{
use Queueable;
public function __construct(public Task $task, public Comment $comment) {}
public function via($notifiable): array
{
return ['database'];
}
public function toArray($notifiable): array
{
return [
'type' => 'task_comment',
'task_id' => $this->task->id,
'project_id' => $this->task->project_id,
'comment_id' => $this->comment->id,
'message' => "Nuevo comentario en '{$this->task->title}' por {$this->comment->user->name}: ".substr($this->comment->body, 0, 100),
];
}
}
@@ -0,0 +1,30 @@
<?php
namespace App\Notifications;
use App\Models\Task;
use Illuminate\Bus\Queueable;
use Illuminate\Notifications\Notification;
class TaskOverdueNotification extends Notification
{
use Queueable;
public function __construct(public Task $task) {}
public function via($notifiable): array
{
return ['database'];
}
public function toArray($notifiable): array
{
return [
'type' => 'task_overdue',
'task_id' => $this->task->id,
'project_id' => $this->task->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')})",
];
}
}
@@ -0,0 +1,45 @@
<?php
namespace App\Notifications;
use App\Models\Task;
use Illuminate\Bus\Queueable;
use Illuminate\Notifications\Notification;
class TaskStatusChangedNotification extends Notification
{
use Queueable;
public function __construct(public Task $task, public string $oldStatus) {}
public function via($notifiable): array
{
return ['database'];
}
public function toArray($notifiable): array
{
$label = [
'pending' => 'pendiente',
'in_progress' => 'en progreso',
'completed' => 'completada',
'cancelled' => 'cancelada',
][$this->task->status] ?? $this->task->status;
$oldLabel = [
'pending' => 'pendiente',
'in_progress' => 'en progreso',
'completed' => 'completada',
'cancelled' => 'cancelada',
][$this->oldStatus] ?? $this->oldStatus;
return [
'type' => 'task_status_changed',
'task_id' => $this->task->id,
'project_id' => $this->task->project_id,
'old_status' => $this->oldStatus,
'new_status' => $this->task->status,
'message' => "La tarea '{$this->task->title}' cambió de {$oldLabel} a {$label}",
];
}
}