TaskStatus::class, 'labor_cost' => 'decimal:2', 'estimated_hours' => 'decimal:2', 'actual_hours' => 'decimal:2', 'completion_percentage' => 'decimal:2', 'start_date' => 'date', 'end_date' => 'date', 'actual_start_date' => 'date', 'actual_end_date' => 'date', ]; } public function project(): BelongsTo { return $this->belongsTo(Project::class); } public function users(): BelongsToMany { return $this->belongsToMany(User::class); } public function taskMaterials(): HasMany { return $this->hasMany(TaskMaterial::class); } public function activities(): HasMany { return $this->hasMany(TaskActivity::class)->orderBy('created_at', 'desc'); } public function delays(): HasMany { return $this->hasMany(TaskDelay::class)->orderByDesc('delay_date'); } public function getTotalDelayDaysAttribute(): float { return round((float) $this->delays->sum('lost_hours') / 8, 1); } public function getWeatherDelayCountAttribute(): int { return $this->delays->where('reason_type', DelayReason::Weather)->count(); } public function getMaterialCostAttribute(): float { return $this->taskMaterials->sum(fn (TaskMaterial $tm) => $tm->effective_cost); } public function getTotalCostAttribute(): float { return (float) $this->labor_cost + $this->material_cost; } public function transitionTo(TaskStatus $newStatus): void { $currentStatus = $this->status; if (!in_array($newStatus, $currentStatus->allowedTransitions())) { throw new \InvalidArgumentException( "Cannot transition task from {$currentStatus->label()} to {$newStatus->label()}" ); } $updates = ['status' => $newStatus]; if ($newStatus === TaskStatus::InProgress && !$this->actual_start_date) { $updates['actual_start_date'] = now(); } if ($newStatus === TaskStatus::Completed) { $updates['actual_end_date'] = now(); $updates['completion_percentage'] = 100; } $this->update($updates); } public function scopeStatus($query, TaskStatus $status) { return $query->where('status', $status); } }