346 lines
13 KiB
Markdown
346 lines
13 KiB
Markdown
# 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`
|
||
|
|
```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)
|
||
|
|
```php
|
||
|
|
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`:
|
||
|
|
```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)
|
||
|
|
|
||
|
|
```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`
|
||
|
|
```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?
|