323 خطوط
14 KiB
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'],
|
|
];
|
|
}
|
|
}
|