- 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
13 KiB
13 KiB
Plan: Task Management Feature (Creación y Asignación de Tareas)
Análisis del Estado Actual
Modelos Existentes Relacionados
- Issue + IssueTask: Sistema de incidencias con checklist de subtareas (ya implementado)
- User: Con roles/permisos (Spatie), asignación a proyectos vía pivot
project_user - Project: Con scope
accessibleBy()para filtrado por permisos - Notifications: Sistema de notificaciones Laravel nativo
Diferencia Clave: IssueTask vs Nueva Tarea
| Aspecto | IssueTask (existente) | Nueva Tarea (propuesta) |
|---|---|---|
| Contexto | Dentro de una Issue (incidencia) | Independiente, a nivel proyecto o global |
| Propósito | Checklist de resolución de incidencia | Gestión de trabajo planificado/operativo |
| Asignación | assigned_to en IssueTask |
assigned_to + posible assigned_by |
| Flujo de estado | is_done (boolean) |
Estados: pending → in_progress → completed / cancelled |
1. Modelo de Datos
Nueva Migración: create_tasks_table.php
Schema::create('tasks', function (Blueprint $table) {
$table->id();
$table->foreignId('project_id')->nullable()->constrained()->cascadeOnDelete();
$table->foreignId('phase_id')->nullable()->constrained()->nullOnDelete(); // opcional: vincular a fase
$table->foreignId('parent_task_id')->nullable()->constrained('tasks')->nullOnDelete(); // subtareas
$table->string('title');
$table->text('description')->nullable();
$table->enum('status', ['pending', 'in_progress', 'completed', 'cancelled'])->default('pending');
$table->enum('priority', ['low', 'medium', 'high', 'critical'])->default('medium');
$table->foreignId('created_by')->constrained('users')->cascadeOnDelete();
$table->foreignId('assigned_to')->nullable()->constrained('users')->nullOnDelete();
$table->date('due_date')->nullable();
$table->date('start_date')->nullable();
$table->datetime('completed_at')->nullable();
$table->foreignId('completed_by')->nullable()->constrained('users')->nullOnDelete();
$table->unsignedInteger('estimated_hours')->nullable();
$table->unsignedInteger('actual_hours')->nullable();
$table->integer('order')->default(0); // para ordenar en listas/Kanban
$table->uuid('uuid')->unique(); // para sync offline
$table->datetime('client_updated_at')->nullable(); // para sync offline
$table->softDeletes();
$table->timestamps();
$table->index(['project_id', 'status']);
$table->index(['assigned_to', 'status']);
$table->index(['due_date', 'status']);
$table->index(['parent_task_id']);
});
Relaciones en Modelos
Task.php (nuevo)
class Task extends Model {
use HasFactory, SoftDeletes, LogsActivity;
protected $fillable = [/* todos los campos */];
protected $casts = [
'due_date' => 'date',
'start_date' => 'date',
'completed_at' => 'datetime',
'estimated_hours' => 'integer',
'actual_hours' => 'integer',
];
public function project() { return $this->belongsTo(Project::class); }
public function phase() { return $this->belongsTo(Phase::class); }
public function creator() { return $this->belongsTo(User::class, 'created_by'); }
public function assignee() { return $this->belongsTo(User::class, 'assigned_to'); }
public function completer() { return $this->belongsTo(User::class, 'completed_by'); }
public function parent() { return $this->belongsTo(Task::class, 'parent_task_id'); }
public function subtasks() { return $this->hasMany(Task::class, 'parent_task_id')->orderBy('order'); }
public function media() { return $this->morphMany(Media::class, 'mediable'); }
public function comments() { return $this->morphMany(Comment::class, 'commentable'); } // ver punto 6
// Scopes
public function scopePending($q) { return $q->where('status', 'pending'); }
public function scopeInProgress($q) { return $q->where('status', 'in_progress'); }
public function scopeCompleted($q) { return $q->where('status', 'completed'); }
public function scopeOverdue($q) { return $q->where('status', '!=', 'completed')->whereDate('due_date', '<', now()); }
// Accessors
public function getIsOverdueAttribute(): bool { ... }
public function getProgressAttribute(): int { /* si tiene subtareas: done/total */ }
}
Actualizaciones a modelos existentes:
Project.php:public function tasks() { return $this->hasMany(Task::class); }Phase.php:public function tasks() { return $this->hasMany(Task::class); }User.php:public function createdTasks() { return $this->hasMany(Task::class, 'created_by'); } public function assignedTasks() { return $this->hasMany(Task::class, 'assigned_to'); } public function completedTasks() { return $this->hasMany(Task::class, 'completed_by'); }
2. Permisos (Spatie)
| Permiso | Descripción |
|---|---|
view tasks |
Ver tareas en proyectos asignados |
create tasks |
Crear tareas en proyectos donde tiene rol |
edit tasks |
Editar tareas propias o del proyecto |
delete tasks |
Eliminar tareas (soft delete) |
assign tasks |
Cambiar assigned_to de cualquier tarea |
manage all tasks |
Admin: ver/editar todas las tareas |
Se añaden al seeder RolesAndPermissionsSeeder.php.
3. Livewire Components
Estructura de Archivos
app/Livewire/Tasks/
├── TaskManager.php # Lista principal con filtros, tabs (Kanban/List)
├── TaskForm.php # Create/Edit modal o page
├── TaskDetail.php # Vista detalle + comentarios + subtareas
├── TaskKanban.php # Vista Kanban por estado (drag & drop)
├── TaskCalendar.php # Vista calendario (opcional, fase 2)
└── MyTasks.php # Widget dashboard: mis tareas asignadas
TaskManager (Lista Principal)
- Filtros: proyecto, fase, asignado, estado, prioridad, vencidas, búsqueda texto
- Vistas alternables: Tabla / Kanban / Lista simple
- Acciones masivas: asignar, cambiar estado, eliminar, exportar
- Paginación + ordenación por columnas
TaskForm
- Campos: título, descripción (rich text), proyecto, fase, padre (para subtareas), prioridad, fechas, horas estimadas, asignado
- Validación:
due_date >= start_date, proyecto accesible por usuario - Modal para crear rápido desde lista
TaskDetail
- Info completa + barra de progreso (subtareas)
- Timeline de cambios (activity log)
- Comentarios (thread style)
- Adjuntos (media)
- Subtareas inline (crear/editar/marcar done)
- Botones acción: Iniciar, Completar, Reabrir, Cancelar
TaskKanban
- Columnas: Pendiente | En Progreso | Completado | Cancelado
- Drag & drop para cambiar estado (Livewire
wire:model+ Alpine/SortableJS) - Tarjetas: título, prioridad (color), asignado (avatar), due date (rojo si vencida)
- Límite WIP configurable por columna (opcional)
MyTasks (Dashboard Widget)
- Ya existe lógica en
routes/web.phplíneas 37-42 paraIssueTask - Extender para incluir
Task::where('assigned_to', $user->id)->where('status', '!=', 'completed') - Vista unificada: "Mis Tareas" (IssueTasks + Tasks)
4. Rutas (routes/web.php)
// Tasks - nivel proyecto
Route::prefix('projects/{project}')->name('projects.')->group(function () {
Route::get('tasks', \App\Livewire\Tasks\TaskManager::class)->name('tasks');
Route::get('tasks/create', \App\Livewire\Tasks\TaskForm::class)->name('tasks.create');
Route::get('tasks/{task}', \App\Livewire\Tasks\TaskDetail::class)->name('tasks.show');
Route::get('tasks/{task}/edit', \App\Livewire\Tasks\TaskForm::class)->name('tasks.edit');
Route::get('tasks/kanban', \App\Livewire\Tasks\TaskKanban::class)->name('tasks.kanban');
});
// Tasks globales (admin/manager)
Route::prefix('tasks')->name('tasks.')->middleware('can:view tasks')->group(function () {
Route::get('/', \App\Livewire\Tasks\TaskManager::class)->name('index');
Route::get('/kanban', \App\Livewire\Tasks\TaskKanban::class)->name('kanban');
Route::get('/calendar', \App\Livewire\Tasks\TaskCalendar::class)->name('calendar');
});
5. Notificaciones y Eventos
Eventos
TaskCreated→ notificar aassigned_toTaskAssigned→ notificar al nuevo asignadoTaskStatusChanged→ notificar acreated_byyassigned_toTaskOverdue→ job diario que notifica tareas vencidasTaskCommentAdded→ notificar a participantes del hilo
Notification Classes
TaskAssignedNotification(implements ShouldQueue)TaskStatusChangedNotificationTaskOverdueNotificationTaskCommentNotification
Canales
- Database (campana notificaciones)
- Email (opcional, configurable por usuario)
6. Comentarios y Adjuntos (Polimórficos)
Reutilizar patrón Media (ya existe morphMany). Para comentarios, crear:
Migración: create_comments_table.php
$table->id();
$table->morphs('commentable'); // task, issue, project, etc.
$table->foreignId('user_id')->constrained()->cascadeOnDelete();
$table->text('body');
$table->foreignId('parent_id')->nullable()->constrained('comments')->nullOnDelete(); // respuestas
$table->timestamps();
Modelo: Comment.php con trait LogsActivity
Uso en Task: $task->comments()->with('user')->latest()->get()
7. Sync Offline (Existente)
Añadir Task a OfflineSyncController:
pendingtable: incluirtasksen modelos sincronizablesuuid+client_updated_atya previstos en migración- Reglas de conflicto:
server_updated_at > client_updated_atgana servidor
8. Tests (Pest/PHPUnit)
| Test | Descripción |
|---|---|
TaskModelTest |
CRUD, scopes, accessors, relaciones |
TaskPolicyTest |
Permisos por rol (admin, manager, viewer, client) |
TaskManagerTest |
Filtros, paginación, vista tabla/kanban |
TaskFormTest |
Validaciones, crear/editar, subtareas |
TaskKanbanTest |
Drag & drop cambio estado, límites WIP |
TaskNotificationTest |
Eventos disparan notificaciones correctas |
TaskSyncTest |
Offline sync create/update/delete/conflict |
9. Internacionalización (i18n)
Añadir keys a lang/{en,es,fr,ru}.json:
tasks.*(title, description, status., priority., due_date, assigned_to, subtasks, etc.)- Notificaciones:
notifications.task_assigned,notifications.task_overdue, etc.
10. Fases de Implementación
Fase 1 - Core (MVP) ⏱️ ~3-4 días
- Migración + Model
Task+ relaciones - Permisos en seeder
TaskManager(tabla + filtros básicos)TaskForm(create/edit modal)TaskDetail(vista + acciones estado)- Rutas + navegación (sidebar)
- Tests básicos
Fase 2 - Kanban + UX ⏱️ ~2 días
TaskKanbancon drag & drop (SortableJS)MyTaskswidget en dashboard (unificar con IssueTask)- Subtareas inline en TaskDetail
- Comentarios polimórficos
Fase 3 - Notificaciones + Avanzado ⏱️ ~2 días
- Eventos + Notifications + Email
- Job diario overdue
- Vista calendario (FullCalendar)
- Exportar CSV/Excel
Fase 4 - Pulido ⏱️ ~1 día
- i18n completo
- Tests E2E (Pest + Browser)
- Documentación en README
- Code review + merge
11. Consideraciones de Diseño
UI/UX
- Colores prioridad: low=gris, medium=ámbar, high=rojo, critical=morado (consistente con Issue)
- Iconos: Heroicons (ya usados en proyecto)
- Responsive: Tailwind breakpoints, tabla → tarjetas en móvil
- Accesibilidad: ARIA en Kanban, focus management en modales
Rendimiento
- Eager loading:
with(['project', 'assignee', 'phase', 'subtasks']) - Índices compuestos en migración
- Paginación 25/50/100
- Lazy loading en Kanban (columnas virtualizadas si >100 tareas)
Seguridad
- Policy
TaskPolicyconviewAny,view,create,update,delete,assign,restore,forceDelete - Scope
accessibleByen Project reutilizable para Task - Validación de pertenencia a proyecto en FormRequest
12. Archivos a Crear/Modificar (Resumen)
Nuevos
database/migrations/xxxx_create_tasks_table.phpdatabase/migrations/xxxx_create_comments_table.phpapp/Models/Task.phpapp/Models/Comment.phpapp/Policies/TaskPolicy.phpapp/Notifications/TaskAssignedNotification.phpapp/Notifications/TaskStatusChangedNotification.phpapp/Notifications/TaskOverdueNotification.phpapp/Notifications/TaskCommentNotification.phpapp/Jobs/NotifyOverdueTasks.phpapp/Livewire/Tasks/TaskManager.php+ bladeapp/Livewire/Tasks/TaskForm.php+ bladeapp/Livewire/Tasks/TaskDetail.php+ bladeapp/Livewire/Tasks/TaskKanban.php+ bladeapp/Livewire/Tasks/MyTasks.php+ blade (widget)tests/Feature/Task*.php(múltiples)
Modificados
database/seeders/RolesAndPermissionsSeeder.php(permisos)app/Models/Project.php(relación tasks)app/Models/Phase.php(relación tasks)app/Models/User.php(relaciones tasks)routes/web.php(rutas tasks)lang/{en,es,fr,ru}.json(traducciones)resources/views/layouts/navigation.blade.php(link sidebar)resources/views/dashboard.blade.php(widget MyTasks)app/Http/Controllers/OfflineSyncController.php(sync tasks)
Próximo Paso
¿Quieres que inicie la Fase 1 creando la migración, el modelo Task, los permisos y el TaskManager básico?