181 lines
4.8 KiB
PHP
181 lines
4.8 KiB
PHP
<?php
|
|
|
|
namespace App\Models;
|
|
|
|
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
|
use Illuminate\Database\Eloquent\Model;
|
|
use Illuminate\Database\Eloquent\SoftDeletes;
|
|
|
|
class Task extends Model
|
|
{
|
|
use HasFactory, SoftDeletes;
|
|
|
|
protected $fillable = [
|
|
'tenant_id',
|
|
'project_id',
|
|
'parent_id',
|
|
'assignee_id',
|
|
'reporter_id',
|
|
'title',
|
|
'description',
|
|
'status',
|
|
'priority',
|
|
'start_date',
|
|
'due_date',
|
|
'progress',
|
|
'estimated_hours',
|
|
'completed_at',
|
|
'order',
|
|
];
|
|
|
|
protected $casts = [
|
|
'start_date' => 'date',
|
|
'due_date' => 'date',
|
|
'completed_at' => 'datetime',
|
|
'progress' => 'integer',
|
|
'estimated_hours' => 'decimal:2',
|
|
'order' => 'integer',
|
|
];
|
|
|
|
// ─── Basic Relationships ───────────────────────────────────────────
|
|
|
|
public function tenant()
|
|
{
|
|
return $this->belongsTo(Tenant::class);
|
|
}
|
|
|
|
public function project()
|
|
{
|
|
return $this->belongsTo(Project::class);
|
|
}
|
|
|
|
public function parent()
|
|
{
|
|
return $this->belongsTo(Task::class, 'parent_id');
|
|
}
|
|
|
|
public function children()
|
|
{
|
|
return $this->hasMany(Task::class, 'parent_id');
|
|
}
|
|
|
|
public function assignee()
|
|
{
|
|
return $this->belongsTo(User::class, 'assignee_id');
|
|
}
|
|
|
|
public function reporter()
|
|
{
|
|
return $this->belongsTo(User::class, 'reporter_id');
|
|
}
|
|
|
|
public function logs()
|
|
{
|
|
return $this->hasMany(TaskLog::class);
|
|
}
|
|
|
|
public function workLogs()
|
|
{
|
|
return $this->hasMany(WorkLog::class);
|
|
}
|
|
|
|
// ─── Dependency Relationships ──────────────────────────────────────
|
|
|
|
/**
|
|
* Tasks that this task depends on (predecessors).
|
|
* e.g., "Task A depends on Task B" means B must finish before A starts.
|
|
*/
|
|
public function dependencies()
|
|
{
|
|
return $this->belongsToMany(Task::class, 'task_dependencies', 'task_id', 'depends_on_task_id')
|
|
->withPivot('dependency_type')
|
|
->withTimestamps();
|
|
}
|
|
|
|
/**
|
|
* Tasks that depend on this task (successors).
|
|
* e.g., "Task A blocks Task C" means C cannot start until A finishes.
|
|
*/
|
|
public function dependents()
|
|
{
|
|
return $this->belongsToMany(Task::class, 'task_dependencies', 'depends_on_task_id', 'task_id')
|
|
->withPivot('dependency_type')
|
|
->withTimestamps();
|
|
}
|
|
|
|
// ─── Scopes ────────────────────────────────────────────────────────
|
|
|
|
public function scopeByStatus($query, $status)
|
|
{
|
|
return $query->where('status', $status);
|
|
}
|
|
|
|
public function scopeByPriority($query, $priority)
|
|
{
|
|
return $query->where('priority', $priority);
|
|
}
|
|
|
|
public function scopeTopLevel($query)
|
|
{
|
|
return $query->whereNull('parent_id');
|
|
}
|
|
|
|
public function scopeWithDependencies($query)
|
|
{
|
|
return $query->with(['dependencies', 'dependents', 'assignee', 'project']);
|
|
}
|
|
|
|
// ─── Accessors ─────────────────────────────────────────────────────
|
|
|
|
public function getStatusLabelAttribute()
|
|
{
|
|
return [
|
|
'pending' => __('task.status_pending'),
|
|
'todo' => __('task.status_pending'),
|
|
'in_progress' => __('task.status_in_progress'),
|
|
'review' => __('task.status_review'),
|
|
'done' => __('task.status_done'),
|
|
'cancelled' => __('task.status_cancelled'),
|
|
][$this->status] ?? $this->status;
|
|
}
|
|
|
|
/**
|
|
* Check if all dependency tasks are completed.
|
|
*/
|
|
public function getCanStartAttribute(): bool
|
|
{
|
|
if ($this->dependencies->isEmpty()) {
|
|
return true;
|
|
}
|
|
|
|
return $this->dependencies->every(fn ($dep) => $dep->status === 'done');
|
|
}
|
|
|
|
/**
|
|
* Check if this task is blocked by incomplete dependencies.
|
|
*/
|
|
public function getIsBlockedAttribute(): bool
|
|
{
|
|
return !$this->canStart;
|
|
}
|
|
|
|
/**
|
|
* Get the blocking tasks (incomplete dependencies).
|
|
*/
|
|
public function getBlockingTasksAttribute()
|
|
{
|
|
return $this->dependencies->filter(fn ($dep) => $dep->status !== 'done');
|
|
}
|
|
|
|
/**
|
|
* Calculate duration in days.
|
|
*/
|
|
public function getDurationDaysAttribute(): ?int
|
|
{
|
|
if (!$this->start_date || !$this->due_date) {
|
|
return null;
|
|
}
|
|
return $this->start_date->diffInDays($this->due_date) + 1;
|
|
}
|
|
}
|