'date', 'start_date' => 'date', 'completed_at' => 'datetime', 'estimated_hours' => 'integer', 'actual_hours' => 'integer', 'order' => 'integer', ]; // ============================================================ // Events / Observers // ============================================================ protected static function booted(): void { static::created(function (Task $task) { if ($task->assigned_to) { $assignee = $task->assignee; if ($assignee && $assignee->id !== auth()->id()) { $assignee->notify(new TaskAssignedNotification($task)); } } }); static::updated(function (Task $task) { // Notify on assignment change if ($task->isDirty('assigned_to')) { $newAssigneeId = $task->assigned_to; if ($newAssigneeId) { $assignee = User::find($newAssigneeId); if ($assignee && $assignee->id !== auth()->id()) { $assignee->notify(new TaskAssignedNotification($task)); } } } // Notify on status change if ($task->isDirty('status')) { $oldStatus = $task->getOriginal('status'); $task->creator?->notify(new TaskStatusChangedNotification($task, $oldStatus)); if ($task->assigned_to && $task->assigned_to !== $task->created_by) { $task->assignee?->notify(new TaskStatusChangedNotification($task, $oldStatus)); } } }); } // ============================================================ // Relationships // ============================================================ public function project(): BelongsTo { return $this->belongsTo(Project::class); } public function phase(): BelongsTo { return $this->belongsTo(Phase::class); } public function parent(): BelongsTo { return $this->belongsTo(Task::class, 'parent_task_id'); } public function subtasks(): HasMany { return $this->hasMany(Task::class, 'parent_task_id')->orderBy('order')->orderBy('id'); } public function creator(): BelongsTo { return $this->belongsTo(User::class, 'created_by'); } public function assignee(): BelongsTo { return $this->belongsTo(User::class, 'assigned_to'); } public function completer(): BelongsTo { return $this->belongsTo(User::class, 'completed_by'); } public function media(): MorphMany { return $this->morphMany(Media::class, 'mediable'); } public function comments(): MorphMany { return $this->morphMany(Comment::class, 'commentable')->orderBy('created_at'); } public function activityLogs(): MorphMany { return $this->morphMany(ActivityLog::class, 'model'); } // ============================================================ // Scopes // ============================================================ public function scopePending($query) { return $query->where('status', 'pending'); } public function scopeInProgress($query) { return $query->where('status', 'in_progress'); } public function scopeCompleted($query) { return $query->where('status', 'completed'); } public function scopeCancelled($query) { return $query->where('status', 'cancelled'); } public function scopeNotCompleted($query) { return $query->where('status', '!=', 'completed'); } public function scopeOverdue($query) { return $query->where('status', '!=', 'completed') ->whereNotNull('due_date') ->whereDate('due_date', '<', now()->toDateString()); } public function scopeDueToday($query) { return $query->where('status', '!=', 'completed') ->whereDate('due_date', now()->toDateString()); } public function scopeAccessibleBy($query, User $user) { if ($user->can('manage all tasks')) { return $query; } return $query->whereHas('project', function ($q) use ($user) { $q->whereHas('users', function ($sub) use ($user) { $sub->where('user_id', $user->id); }); }); } // ============================================================ // Accessors / Helpers // ============================================================ public function getIsOverdueAttribute(): bool { return $this->status !== 'completed' && $this->due_date && $this->due_date->isPast(); } public function getIsDueTodayAttribute(): bool { return $this->status !== 'completed' && $this->due_date && $this->due_date->isToday(); } public function getProgressAttribute(): int { $total = $this->subtasks->count(); if ($total === 0) { return in_array($this->status, ['completed'], true) ? 100 : 0; } return (int) round($this->subtasks->where('status', 'completed')->count() / $total * 100); } public function getPriorityColorAttribute(): string { return match ($this->priority) { 'low' => '#6b7280', // gray 'medium' => '#f59e0b', // amber 'high' => '#ef4444', // red 'critical' => '#7c3aed', // purple default => '#6b7280', }; } public function getStatusColorAttribute(): string { return match ($this->status) { 'pending' => '#6b7280', // gray 'in_progress' => '#3b82f6', // blue 'completed' => '#10b981', // green 'cancelled' => '#ef4444', // red default => '#6b7280', }; } public function getStatusLabelAttribute(): string { return match ($this->status) { 'pending' => 'Pendiente', 'in_progress' => 'En progreso', 'completed' => 'Completada', 'cancelled' => 'Cancelada', default => ucfirst((string) $this->status), }; } public function getPriorityLabelAttribute(): string { return match ($this->priority) { 'low' => 'Baja', 'medium' => 'Media', 'high' => 'Alta', 'critical' => 'Crítica', default => ucfirst((string) $this->priority), }; } // ============================================================ // State Transitions // ============================================================ public function start(): void { $this->update([ 'status' => 'in_progress', 'start_date' => $this->start_date ?? now()->toDateString(), ]); } public function complete(?User $user = null): void { $this->update([ 'status' => 'completed', 'completed_at' => now(), 'completed_by' => $user?->id ?? auth()->id(), ]); } public function reopen(): void { $this->update([ 'status' => 'pending', 'completed_at' => null, 'completed_by' => null, ]); } public function cancel(): void { $this->update(['status' => 'cancelled']); } // ============================================================ // Static Helpers // ============================================================ public static function statusOptions(): array { return [ 'pending' => 'Pendiente', 'in_progress' => 'En progreso', 'completed' => 'Completada', 'cancelled' => 'Cancelada', ]; } public static function priorityOptions(): array { return [ 'low' => 'Baja', 'medium' => 'Media', 'high' => 'Alta', 'critical' => 'Crítica', ]; } }