Files
GSB-Construction/Modules/ProjectManagement/app/Models/Task.php
Ajjj ccbd23d474
Some checks failed
Tests / PHP 8.3 (push) Has been cancelled
Tests / PHP 8.4 (push) Has been cancelled
Tests / PHP 8.5 (push) Has been cancelled
feat: enhance module RBAC permissions, financial billing workflows, task evidence uploading, and role analytics
2026-08-13 18:46:26 +08:00

329 lines
12 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;
use Spatie\MediaLibrary\HasMedia;
use Spatie\MediaLibrary\InteractsWithMedia;
class Task extends Model implements HasMedia
{
use HasFactory, HasPublicIdentifier, InteractsWithMedia;
protected $appends = ['evidence_urls'];
public function getEvidenceUrlsAttribute(): array
{
return $this->getMedia('evidence')->map(function ($media) {
return [
'id' => $media->id,
'name' => $media->name,
'file_name' => $media->file_name,
'original_url' => $media->original_url,
'mime_type' => $media->mime_type,
'size' => $media->size,
];
})->toArray();
}
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'] = 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);
}
}