TaskStatus::class, 'labor_cost' => 'decimal:2', 'equipment_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', ]; } protected static function booted(): void { static::created(function (Task $task) { if (auth()->check()) { $task->activities()->create([ 'user_id' => auth()->id(), 'description' => 'created the task.', 'type' => 'creation', ]); } }); static::updating(function (Task $task) { if (auth()->check()) { $dirty = $task->getDirty(); $ignored = ['sort_order', 'updated_at', 'created_at']; $changes = []; foreach ($dirty as $key => $value) { if (in_array($key, $ignored)) { continue; } $original = $task->getOriginal($key); $label = str_replace('_', ' ', $key); if ($key === 'status') { $origLabel = $original instanceof TaskStatus ? $original->label() : $original; $newLabel = $value instanceof TaskStatus ? $value->label() : $value; $changes[] = "changed status from '{$origLabel}' to '{$newLabel}'"; } elseif ($key === 'milestone_id') { $origMilestone = $original ? (ProjectMilestone::find($original)?->name ?? 'Unknown') : 'None'; $newMilestone = $value ? (ProjectMilestone::find($value)?->name ?? 'Unknown') : 'None'; $changes[] = "changed milestone from '{$origMilestone}' to '{$newMilestone}'"; } elseif ($key === 'labor_cost') { $changes[] = "changed labor cost from ₱" . number_format((float)$original, 2) . " to ₱" . number_format((float)$value, 2); } elseif ($key === 'equipment_cost') { $changes[] = "changed equipment cost from ₱" . number_format((float)$original, 2) . " to ₱" . number_format((float)$value, 2); } elseif ($key === 'estimated_hours') { $changes[] = "changed estimated hours from {$original} to {$value}"; } elseif ($key === 'name') { $changes[] = "changed name from '{$original}' to '{$value}'"; } elseif ($key === 'description') { $changes[] = "updated description"; } elseif (in_array($key, ['start_date', 'end_date', 'actual_start_date', 'actual_end_date'])) { $origDate = $original ? ($original instanceof \Carbon\Carbon ? $original->format('Y-m-d') : substr($original, 0, 10)) : 'None'; $newDate = $value ? ($value instanceof \Carbon\Carbon ? $value->format('Y-m-d') : substr($value, 0, 10)) : 'None'; $changes[] = "changed {$label} from '{$origDate}' to '{$newDate}'"; } elseif ($key === 'completion_percentage') { $changes[] = "updated completion percentage to {$value}%"; } else { $changes[] = "updated {$label} to '{$value}'"; } } if (!empty($changes)) { $task->pending_activities_log = $changes; } } }); static::updated(function (Task $task) { if (auth()->check() && isset($task->pending_activities_log) && !empty($task->pending_activities_log)) { $description = implode(', ', $task->pending_activities_log) . '.'; if ($description !== '.') { $task->activities()->create([ 'user_id' => auth()->id(), 'description' => $description, 'type' => 'update', ]); } unset($task->pending_activities_log); } }); } public function project(): BelongsTo { return $this->belongsTo(Project::class); } public function milestone(): BelongsTo { return $this->belongsTo(ProjectMilestone::class, 'milestone_id'); } public function users(): BelongsToMany { return $this->belongsToMany(User::class); } public function taskMaterials(): HasMany { return $this->hasMany(TaskMaterial::class); } public function taskLabors(): HasMany { return $this->hasMany(TaskLabor::class); } public function taskEquipments(): HasMany { return $this->hasMany(TaskEquipment::class); } public function taskIssues(): HasMany { return $this->hasMany(TaskIssue::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 getEstimatedLaborCostAttribute(): float { $resourceSum = (float) $this->taskLabors->sum(fn ($tl) => $tl->estimated_cost); return $resourceSum > 0 ? $resourceSum : (float) $this->labor_cost; } public function getActualLaborCostAttribute(): float { $resourceSum = (float) $this->taskLabors->sum(fn ($tl) => $tl->actual_cost); if ($resourceSum > 0) { return $resourceSum; } return ($this->status === TaskStatus::Completed || $this->status === TaskStatus::Closed) ? (float) $this->labor_cost : 0.00; } public function getEstimatedEquipmentCostAttribute(): float { $resourceSum = (float) $this->taskEquipments->sum(fn ($te) => $te->estimated_cost); return $resourceSum > 0 ? $resourceSum : (float) $this->equipment_cost; } public function getActualEquipmentCostAttribute(): float { $resourceSum = (float) $this->taskEquipments->sum(fn ($te) => $te->actual_cost); if ($resourceSum > 0) { return $resourceSum; } return ($this->status === TaskStatus::Completed || $this->status === TaskStatus::Closed) ? (float) $this->equipment_cost : 0.00; } public function getTotalCostAttribute(): float { return (float) $this->actual_labor_cost + (float) $this->actual_equipment_cost + (float) $this->material_cost; } public function recalculateTaskProgress(): void { $plannedMaterials = $this->taskMaterials->sum('planned_qty'); $actualMaterials = $this->taskMaterials->sum('actual_qty'); $materialProgress = $plannedMaterials > 0 ? min(100, ($actualMaterials / $plannedMaterials) * 100) : null; $plannedLabor = $this->taskLabors->sum('estimated_hours'); $actualLabor = $this->taskLabors->sum('actual_hours'); $laborProgress = $plannedLabor > 0 ? min(100, ($actualLabor / $plannedLabor) * 100) : null; $plannedEquipment = $this->taskEquipments->sum('estimated_hours'); $actualEquipment = $this->taskEquipments->sum('actual_hours'); $equipmentProgress = $plannedEquipment > 0 ? min(100, ($actualEquipment / $plannedEquipment) * 100) : null; $metrics = array_filter([$materialProgress, $laborProgress, $equipmentProgress], fn($v) => !is_null($v)); if (count($metrics) > 0) { $newProgress = array_sum($metrics) / count($metrics); } else { $newProgress = match($this->status) { TaskStatus::Completed, TaskStatus::Closed => 100.0, TaskStatus::InProgress => 20.0, default => 0.0 }; } $this->update(['completion_percentage' => round($newProgress, 2)]); $this->project->update([ 'completion_percentage' => $this->project->milestone_completion ]); } 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) { if (!$this->actual_start_date) { $updates['actual_start_date'] = now(); } if ($currentStatus === TaskStatus::Completed) { $updates['actual_end_date'] = null; } $updates['completion_percentage'] = 50; } if ($newStatus === TaskStatus::Pending) { $updates['actual_start_date'] = null; $updates['actual_end_date'] = null; $updates['completion_percentage'] = 0; } if ($newStatus === TaskStatus::Completed || $newStatus === TaskStatus::Closed) { if (!$this->actual_end_date) { $updates['actual_end_date'] = now(); } $updates['completion_percentage'] = 100; } $this->update($updates); } public function scopeStatus($query, TaskStatus $status) { return $query->where('status', $status); } }