PM_Console/backend/app/Services/DashboardService.php

323 خطوط
14 KiB
PHP

<?php
namespace App\Services;
use App\Models\ActionItem;
use App\Models\ActivityLog;
use App\Models\Blocker;
use App\Models\Meeting;
use App\Models\Project;
use App\Models\Sprint;
use App\Models\Task;
use App\Models\User;
use Carbon\Carbon;
class DashboardService
{
public function getSummary($user = null)
{
$activeProjects = Project::where('status', '!=', 'done')->where('is_archived', false)->count();
$completedProjects = Project::where('status', 'done')->count();
$today = Carbon::today();
$delayedProjects = Project::whereDate('end_date', '<', $today)->where('status', '!=', 'done')->where('is_archived', false)->count();
$openTasks = Task::whereNotIn('status', ['done', 'canceled'])->count();
$completedTasks = Task::where('status', 'done')->count();
$delayedTasks = Task::whereDate('due_date', '<', $today)->where('status', '!=', 'done')->count();
$teamPerformance = [];
$workload = [];
$riskyProjects = Project::whereIn('risk_level', ['high', 'critical'])->where('is_archived', false)->limit(5)->get();
$upcomingDeadlines = Task::with('project')
->whereDate('due_date', '>=', $today)
->whereDate('due_date', '<=', $today->copy()->addDays(7))
->where('status', '!=', 'done')
->orderBy('due_date')
->limit(10)
->get()
->map(fn ($t) => [
'id' => $t->id,
'title' => $t->title,
'project' => $t->project?->title,
'due_date' => $t->due_date?->format('Y-m-d'),
'days_left' => $today->diffInDays($t->due_date, false),
]);
$recentActivities = ActivityLog::with('user')
->orderBy('created_at', 'desc')
->limit(10)
->get()
->map(fn ($log) => [
'id' => $log->id,
'description' => $log->description,
'user' => $log->user?->name ?? '?',
'created_at' => $log->created_at?->toDateTimeString(),
'time_ago' => $log->created_at?->diffForHumans(),
]);
$myTasks = collect();
if ($user) {
$myTasks = Task::where('assignee_id', $user->id)
->where('status', '!=', 'done')
->orderBy('due_date')
->limit(10)
->get();
}
$activeSprint = null;
if ($user) {
$sprint = Sprint::with(['project', 'tasks.assignee'])
->where('status', 'active')
->whereHas('members', fn ($query) => $query->where('users.id', $user->id))
->orderBy('end_date')
->first();
if ($sprint) {
$totalTasks = $sprint->tasks->count();
$completedTasks = $sprint->tasks->where('status', 'done')->count();
$activeSprint = [
'id' => $sprint->id,
'title' => $sprint->title,
'project' => $sprint->project?->title,
'end_date' => $sprint->end_date?->format('Y-m-d'),
'remaining_days' => max(Carbon::today()->diffInDays($sprint->end_date, false), 0),
'progress' => $totalTasks > 0 ? round(($completedTasks / $totalTasks) * 100, 2) : 0,
'my_tasks' => $sprint->tasks
->where('assignee_id', $user->id)
->values()
->map(fn ($task) => [
'id' => $task->id,
'title' => $task->title,
'status' => $task->status,
]),
];
}
}
$todayMeetings = Meeting::whereDate('date', Carbon::today())->count();
$teamMembers = User::count();
$projectProgresses = Project::where('is_archived', false)
->select(['id', 'title', 'progress', 'status'])
->limit(10)
->get();
return [
'activeProjects' => $activeProjects,
'completedProjects' => $completedProjects,
'delayedProjects' => $delayedProjects,
'openTasks' => $openTasks,
'completedTasks' => $completedTasks,
'delayedTasks' => $delayedTasks,
'teamPerformance' => $teamPerformance,
'workload' => $workload,
'riskyProjects' => $riskyProjects,
'upcomingDeadlines' => $upcomingDeadlines,
'recentActivities' => $recentActivities,
'myTasks' => $myTasks,
'todayMeetings' => $todayMeetings,
'teamMembers' => $teamMembers,
'projectProgresses' => $projectProgresses,
'activeSprint' => $activeSprint,
];
}
public function getChartData()
{
$months = collect();
$taskCreationData = [];
$taskCompletionData = [];
for ($i = 5; $i >= 0; $i--) {
$month = Carbon::now()->subMonths($i);
$monthName = $month->format('Y-m');
$months->push($monthName);
$taskCreationData[] = Task::whereYear('created_at', $month->year)
->whereMonth('created_at', $month->month)
->count();
$taskCompletionData[] = Task::where('status', 'done')
->whereYear('updated_at', $month->year)
->whereMonth('updated_at', $month->month)
->count();
}
$monthlyTasks = [];
foreach ($months as $i => $month) {
$monthlyTasks[] = [
'month' => $month,
'created' => $taskCreationData[$i],
'completed' => $taskCompletionData[$i],
];
}
return [
'labels' => $months->values(),
'taskCreation' => $taskCreationData,
'taskCompletion' => $taskCompletionData,
'monthlyTasks' => $monthlyTasks,
'monthly_tasks' => $monthlyTasks,
];
}
public function getMonitoring($user = null): array
{
$today = Carbon::today();
$weekEnd = $today->copy()->addDays(7);
$projects = Project::query()
->where('is_archived', false)
->withCount([
'tasks as total_tasks_count',
'tasks as completed_tasks_count' => fn ($query) => $query->where('status', 'done'),
'tasks as overdue_tasks_count' => fn ($query) => $query
->whereDate('due_date', '<', $today)
->whereNotIn('status', ['done', 'canceled']),
'tasks as blocked_tasks_count' => fn ($query) => $query->where('is_blocked', true),
])
->orderByRaw("CASE WHEN risk_level = 'critical' THEN 0 WHEN risk_level = 'high' THEN 1 ELSE 2 END")
->limit(8)
->get()
->map(function (Project $project) {
$health = 'on_track';
if ($project->risk_level === 'critical' || $project->overdue_tasks_count >= 5) {
$health = 'off_track';
} elseif ($project->risk_level === 'high' || $project->overdue_tasks_count > 0 || $project->blocked_tasks_count > 0) {
$health = 'at_risk';
}
return [
'id' => $project->id,
'title' => $project->title,
'status' => $project->status,
'risk_level' => $project->risk_level,
'progress' => $project->progress,
'health' => $health,
'total_tasks' => $project->total_tasks_count,
'completed_tasks' => $project->completed_tasks_count,
'overdue_tasks' => $project->overdue_tasks_count,
'blocked_tasks' => $project->blocked_tasks_count,
];
});
$sprints = Sprint::with(['project:id,title'])
->withCount([
'tasks as total_tasks_count',
'tasks as completed_tasks_count' => fn ($query) => $query->where('status', 'done'),
'tasks as blocked_tasks_count' => fn ($query) => $query->where('is_blocked', true),
])
->where('status', 'active')
->orderBy('end_date')
->limit(6)
->get()
->map(function (Sprint $sprint) use ($today) {
$progress = $sprint->total_tasks_count > 0
? round(($sprint->completed_tasks_count / $sprint->total_tasks_count) * 100)
: 0;
$elapsed = max($sprint->start_date?->diffInDays($today, false) ?? 0, 0);
$duration = max($sprint->start_date?->diffInDays($sprint->end_date) ?? 1, 1);
$expected = min(100, round(($elapsed / $duration) * 100));
$health = $sprint->blocked_tasks_count > 1 || $progress + 20 < $expected ? 'at_risk' : 'on_track';
if ($sprint->end_date?->isPast() && $progress < 100) {
$health = 'off_track';
}
return [
'id' => $sprint->id,
'title' => $sprint->title,
'project' => $sprint->project?->title,
'progress' => $progress,
'expected_progress' => $expected,
'remaining_days' => max($today->diffInDays($sprint->end_date, false), 0),
'health' => $health,
'blocked_tasks' => $sprint->blocked_tasks_count,
];
});
$workload = User::query()
->where('status', 'active')
->withCount([
'tasks as open_tasks_count' => fn ($query) => $query->whereNotIn('status', ['done', 'canceled']),
'tasks as overdue_tasks_count' => fn ($query) => $query
->whereDate('due_date', '<', $today)
->whereNotIn('status', ['done', 'canceled']),
])
->orderByDesc('open_tasks_count')
->limit(8)
->get(['id', 'name'])
->map(fn (User $member) => [
'id' => $member->id,
'name' => $member->name,
'open_tasks' => $member->open_tasks_count,
'overdue_tasks' => $member->overdue_tasks_count,
'load' => $member->open_tasks_count >= 10 ? 'overloaded' : ($member->open_tasks_count >= 6 ? 'high' : 'balanced'),
]);
$attention = collect();
Task::with('project:id,title')
->whereNotIn('status', ['done', 'canceled'])
->where(function ($query) use ($today, $weekEnd) {
$query->where('is_blocked', true)
->orWhereDate('due_date', '<', $today)
->orWhereBetween('due_date', [$today, $weekEnd]);
})
->orderBy('due_date')
->limit(10)
->get()
->each(fn (Task $task) => $attention->push([
'id' => "task-{$task->id}",
'type' => $task->is_blocked ? 'blocker' : ($task->due_date?->isPast() ? 'overdue' : 'deadline'),
'title' => $task->title,
'context' => $task->project?->title,
'date' => $task->due_date?->format('Y-m-d'),
'target_url' => "/tasks?preview={$task->id}",
]));
$upcomingMeetings = Meeting::with(['project:id,title', 'sprint:id,title'])
->where(function ($query) use ($today) {
$query->whereDate('date', '>', $today)
->orWhere(function ($sameDay) use ($today) {
$sameDay->whereDate('date', $today)->whereTime('start_time', '>=', now()->format('H:i:s'));
});
})
->whereNotIn('status', ['cancelled', 'completed'])
->orderBy('date')
->orderBy('start_time')
->limit(5)
->get()
->map(fn (Meeting $meeting) => [
'id' => $meeting->id,
'title' => $meeting->title,
'date' => $meeting->date?->format('Y-m-d'),
'time' => $meeting->start_time ? substr($meeting->start_time, 0, 5) : null,
'project' => $meeting->project?->title,
'sprint' => $meeting->sprint?->title,
'target_url' => "/meetings/{$meeting->id}/workspace",
]);
return [
'generated_at' => now()->toIso8601String(),
'summary' => [
'active_projects' => Project::where('is_archived', false)->where('status', '!=', 'done')->count(),
'projects_at_risk' => $projects->whereIn('health', ['at_risk', 'off_track'])->count(),
'open_tasks' => Task::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(),
'active_sprints' => Sprint::where('status', 'active')->count(),
'meetings_today' => Meeting::whereDate('date', $today)->where('status', '!=', 'cancelled')->count(),
'overdue_action_items' => ActionItem::whereDate('due_date', '<', $today)->whereNotIn('status', ['completed', 'cancelled'])->count(),
'open_blockers' => Blocker::whereNotIn('status', ['resolved', 'closed'])->count(),
],
'projects' => $projects,
'sprints' => $sprints,
'workload' => $workload,
'attention' => $attention->values(),
'upcoming_meetings' => $upcomingMeetings,
'trends' => $this->getChartData()['monthlyTasks'],
];
}
}