Files
construprogress/TASK_FEATURE_PLAN.md
Javier Braña ba614bddbc 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
2026-08-03 13:44:10 +02:00

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: pendingin_progresscompleted / 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.php líneas 37-42 para IssueTask
  • 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 a assigned_to
  • TaskAssigned → notificar al nuevo asignado
  • TaskStatusChanged → notificar a created_by y assigned_to
  • TaskOverdue → job diario que notifica tareas vencidas
  • TaskCommentAdded → notificar a participantes del hilo

Notification Classes

  • TaskAssignedNotification (implements ShouldQueue)
  • TaskStatusChangedNotification
  • TaskOverdueNotification
  • TaskCommentNotification

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:

  • pending table: incluir tasks en modelos sincronizables
  • uuid + client_updated_at ya previstos en migración
  • Reglas de conflicto: server_updated_at > client_updated_at gana 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

  1. Migración + Model Task + relaciones
  2. Permisos en seeder
  3. TaskManager (tabla + filtros básicos)
  4. TaskForm (create/edit modal)
  5. TaskDetail (vista + acciones estado)
  6. Rutas + navegación (sidebar)
  7. Tests básicos

Fase 2 - Kanban + UX ⏱️ ~2 días

  1. TaskKanban con drag & drop (SortableJS)
  2. MyTasks widget en dashboard (unificar con IssueTask)
  3. Subtareas inline en TaskDetail
  4. Comentarios polimórficos

Fase 3 - Notificaciones + Avanzado ⏱️ ~2 días

  1. Eventos + Notifications + Email
  2. Job diario overdue
  3. Vista calendario (FullCalendar)
  4. Exportar CSV/Excel

Fase 4 - Pulido ⏱️ ~1 día

  1. i18n completo
  2. Tests E2E (Pest + Browser)
  3. Documentación en README
  4. 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 TaskPolicy con viewAny, view, create, update, delete, assign, restore, forceDelete
  • Scope accessibleBy en 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.php
  • database/migrations/xxxx_create_comments_table.php
  • app/Models/Task.php
  • app/Models/Comment.php
  • app/Policies/TaskPolicy.php
  • app/Notifications/TaskAssignedNotification.php
  • app/Notifications/TaskStatusChangedNotification.php
  • app/Notifications/TaskOverdueNotification.php
  • app/Notifications/TaskCommentNotification.php
  • app/Jobs/NotifyOverdueTasks.php
  • app/Livewire/Tasks/TaskManager.php + blade
  • app/Livewire/Tasks/TaskForm.php + blade
  • app/Livewire/Tasks/TaskDetail.php + blade
  • app/Livewire/Tasks/TaskKanban.php + blade
  • app/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?