311 lines
11 KiB
PHP
311 lines
11 KiB
PHP
<?php
|
|
|
|
namespace Modules\ProjectManagement\Models;
|
|
use Modules\MasterData\Models\Material;
|
|
use Modules\MasterData\Models\MaterialGroup;
|
|
|
|
use App\Models\User;
|
|
use App\Traits\HasPublicIdentifier;
|
|
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
|
use Illuminate\Database\Eloquent\Model;
|
|
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
|
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
|
|
use Illuminate\Database\Eloquent\Relations\HasMany;
|
|
use Modules\ProjectManagement\Enums\DelayReason;
|
|
use Modules\ProjectManagement\Enums\TaskStatus;
|
|
|
|
class Task extends Model
|
|
{
|
|
use HasFactory, HasPublicIdentifier;
|
|
|
|
protected static function newFactory()
|
|
{
|
|
return \Modules\ProjectManagement\Database\Factories\TaskFactory::new();
|
|
}
|
|
|
|
public ?array $pending_activities_log = null;
|
|
|
|
protected $fillable = [
|
|
'project_id',
|
|
'name',
|
|
'description',
|
|
'area',
|
|
'status',
|
|
'sort_order',
|
|
'labor_cost',
|
|
'equipment_cost',
|
|
'estimated_hours',
|
|
'actual_hours',
|
|
'start_date',
|
|
'end_date',
|
|
'actual_start_date',
|
|
'actual_end_date',
|
|
'completion_percentage',
|
|
'milestone_id',
|
|
];
|
|
|
|
protected function casts(): array
|
|
{
|
|
return [
|
|
'status' => 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'] = 0;
|
|
}
|
|
}
|
|
|
|
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);
|
|
}
|
|
}
|
|
|