fix-dashboard-monitoring-blocker-query
مخزن کامیت contained در
والد
c94bb44d9f
کامیت
0ad1cafcb9
|
|
@ -465,7 +465,8 @@ class SprintController extends Controller
|
||||||
$tasks = $sprint->tasks;
|
$tasks = $sprint->tasks;
|
||||||
$total = $tasks->count();
|
$total = $tasks->count();
|
||||||
$completed = $tasks->where('status', 'done')->count();
|
$completed = $tasks->where('status', 'done')->count();
|
||||||
$blocked = $tasks->where('is_blocked', true)->count() + $sprint->blockers->whereNotIn('status', ['resolved', 'closed'])->count();
|
$blocked = $tasks->filter(fn (Task $task) => $task->is_blocked)->count()
|
||||||
|
+ $sprint->blockers->whereNotIn('status', ['resolved', 'closed'])->count();
|
||||||
$overdue = $tasks->filter(fn (Task $task) => $task->due_date?->isPast() && $task->status !== 'done')->count();
|
$overdue = $tasks->filter(fn (Task $task) => $task->due_date?->isPast() && $task->status !== 'done')->count();
|
||||||
$progress = $total > 0 ? round(($completed / $total) * 100) : 0;
|
$progress = $total > 0 ? round(($completed / $total) * 100) : 0;
|
||||||
$today = Carbon::today();
|
$today = Carbon::today();
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,7 @@
|
||||||
namespace App\Models;
|
namespace App\Models;
|
||||||
|
|
||||||
use Illuminate\Database\Eloquent\Model;
|
use Illuminate\Database\Eloquent\Model;
|
||||||
|
use Illuminate\Database\Eloquent\Builder;
|
||||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||||
use Illuminate\Database\Eloquent\Relations\HasMany;
|
use Illuminate\Database\Eloquent\Relations\HasMany;
|
||||||
use Illuminate\Database\Eloquent\Relations\MorphMany;
|
use Illuminate\Database\Eloquent\Relations\MorphMany;
|
||||||
|
|
@ -23,6 +24,19 @@ class Task extends Model
|
||||||
'actual_time' => 'decimal:2',
|
'actual_time' => 'decimal:2',
|
||||||
];
|
];
|
||||||
|
|
||||||
|
public function scopeBlocked(Builder $query): Builder
|
||||||
|
{
|
||||||
|
return $query->where(function (Builder $blocked) {
|
||||||
|
$blocked->whereNotNull('blocker_type')
|
||||||
|
->orWhereNotNull('blocker_note');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getIsBlockedAttribute(): bool
|
||||||
|
{
|
||||||
|
return filled($this->blocker_type) || filled($this->blocker_note);
|
||||||
|
}
|
||||||
|
|
||||||
public function project(): BelongsTo
|
public function project(): BelongsTo
|
||||||
{
|
{
|
||||||
return $this->belongsTo(Project::class);
|
return $this->belongsTo(Project::class);
|
||||||
|
|
|
||||||
|
|
@ -176,7 +176,7 @@ class DashboardService
|
||||||
'tasks as overdue_tasks_count' => fn ($query) => $query
|
'tasks as overdue_tasks_count' => fn ($query) => $query
|
||||||
->whereDate('due_date', '<', $today)
|
->whereDate('due_date', '<', $today)
|
||||||
->whereNotIn('status', ['done', 'canceled']),
|
->whereNotIn('status', ['done', 'canceled']),
|
||||||
'tasks as blocked_tasks_count' => fn ($query) => $query->where('is_blocked', true),
|
'tasks as blocked_tasks_count' => fn ($query) => $query->blocked(),
|
||||||
])
|
])
|
||||||
->orderByRaw("CASE WHEN risk_level = 'critical' THEN 0 WHEN risk_level = 'high' THEN 1 ELSE 2 END")
|
->orderByRaw("CASE WHEN risk_level = 'critical' THEN 0 WHEN risk_level = 'high' THEN 1 ELSE 2 END")
|
||||||
->limit(8)
|
->limit(8)
|
||||||
|
|
@ -207,7 +207,7 @@ class DashboardService
|
||||||
->withCount([
|
->withCount([
|
||||||
'tasks as total_tasks_count',
|
'tasks as total_tasks_count',
|
||||||
'tasks as completed_tasks_count' => fn ($query) => $query->where('status', 'done'),
|
'tasks as completed_tasks_count' => fn ($query) => $query->where('status', 'done'),
|
||||||
'tasks as blocked_tasks_count' => fn ($query) => $query->where('is_blocked', true),
|
'tasks as blocked_tasks_count' => fn ($query) => $query->blocked(),
|
||||||
])
|
])
|
||||||
->where('status', 'active')
|
->where('status', 'active')
|
||||||
->orderBy('end_date')
|
->orderBy('end_date')
|
||||||
|
|
@ -260,7 +260,7 @@ class DashboardService
|
||||||
Task::with('project:id,title')
|
Task::with('project:id,title')
|
||||||
->whereNotIn('status', ['done', 'canceled'])
|
->whereNotIn('status', ['done', 'canceled'])
|
||||||
->where(function ($query) use ($today, $weekEnd) {
|
->where(function ($query) use ($today, $weekEnd) {
|
||||||
$query->where('is_blocked', true)
|
$query->blocked()
|
||||||
->orWhereDate('due_date', '<', $today)
|
->orWhereDate('due_date', '<', $today)
|
||||||
->orWhereBetween('due_date', [$today, $weekEnd]);
|
->orWhereBetween('due_date', [$today, $weekEnd]);
|
||||||
})
|
})
|
||||||
|
|
@ -305,7 +305,7 @@ class DashboardService
|
||||||
'projects_at_risk' => $projects->whereIn('health', ['at_risk', 'off_track'])->count(),
|
'projects_at_risk' => $projects->whereIn('health', ['at_risk', 'off_track'])->count(),
|
||||||
'open_tasks' => Task::whereNotIn('status', ['done', 'canceled'])->count(),
|
'open_tasks' => Task::whereNotIn('status', ['done', 'canceled'])->count(),
|
||||||
'overdue_tasks' => Task::whereDate('due_date', '<', $today)->whereNotIn('status', ['done', 'canceled'])->count(),
|
'overdue_tasks' => Task::whereDate('due_date', '<', $today)->whereNotIn('status', ['done', 'canceled'])->count(),
|
||||||
'blocked_tasks' => Task::where('is_blocked', true)->whereNotIn('status', ['done', 'canceled'])->count(),
|
'blocked_tasks' => Task::blocked()->whereNotIn('status', ['done', 'canceled'])->count(),
|
||||||
'active_sprints' => Sprint::where('status', 'active')->count(),
|
'active_sprints' => Sprint::where('status', 'active')->count(),
|
||||||
'meetings_today' => Meeting::whereDate('date', $today)->where('status', '!=', 'cancelled')->count(),
|
'meetings_today' => Meeting::whereDate('date', $today)->where('status', '!=', 'cancelled')->count(),
|
||||||
'overdue_action_items' => ActionItem::whereDate('due_date', '<', $today)->whereNotIn('status', ['completed', 'cancelled'])->count(),
|
'overdue_action_items' => ActionItem::whereDate('due_date', '<', $today)->whereNotIn('status', ['completed', 'cancelled'])->count(),
|
||||||
|
|
|
||||||
|
|
@ -8,6 +8,7 @@ use App\Models\MeetingActionItem;
|
||||||
use App\Models\Permission;
|
use App\Models\Permission;
|
||||||
use App\Models\Project;
|
use App\Models\Project;
|
||||||
use App\Models\Role;
|
use App\Models\Role;
|
||||||
|
use App\Models\Task;
|
||||||
use App\Models\User;
|
use App\Models\User;
|
||||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||||
use Illuminate\Http\UploadedFile;
|
use Illuminate\Http\UploadedFile;
|
||||||
|
|
@ -202,4 +203,32 @@ class SecurityHardeningTest extends TestCase
|
||||||
|
|
||||||
$this->assertSame('Private action', $item->fresh()->title);
|
$this->assertSame('Private action', $item->fresh()->title);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function test_dashboard_monitoring_uses_task_blocker_fields(): void
|
||||||
|
{
|
||||||
|
$user = User::factory()->create(['status' => 'active']);
|
||||||
|
$this->grantAdminRole($user);
|
||||||
|
$project = Project::create([
|
||||||
|
'title' => 'Monitoring project',
|
||||||
|
'project_manager_id' => $user->id,
|
||||||
|
'created_by' => $user->id,
|
||||||
|
]);
|
||||||
|
Task::create([
|
||||||
|
'title' => 'Blocked task',
|
||||||
|
'project_id' => $project->id,
|
||||||
|
'reporter_id' => $user->id,
|
||||||
|
'created_by' => $user->id,
|
||||||
|
'status' => 'in_progress',
|
||||||
|
'blocker_type' => 'dependency',
|
||||||
|
]);
|
||||||
|
|
||||||
|
$token = $user->createToken('api-token')->plainTextToken;
|
||||||
|
|
||||||
|
$this->withToken($token)
|
||||||
|
->getJson('/api/dashboard/monitoring')
|
||||||
|
->assertOk()
|
||||||
|
->assertJsonPath('success', true)
|
||||||
|
->assertJsonPath('data.summary.blocked_tasks', 1)
|
||||||
|
->assertJsonPath('data.projects.0.blocked_tasks', 1);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
بارگذاری…
مرجع در شماره جدید