withCount('milestones') ->withTrashed(); if (! empty($filters['search'])) { $search = $filters['search']; $query->where(function ($q) use ($search) { $q->where('title', 'like', "%{$search}%") ->orWhere('description', 'like', "%{$search}%"); }); } if (! empty($filters['status'])) { $query->where('status', $filters['status']); } if (! empty($filters['category'])) { $query->where('category', $filters['category']); } if (! empty($filters['priority'])) { $query->where('priority', $filters['priority']); } if (! empty($filters['phase'])) { $query->where('phase', $filters['phase']); } if (isset($filters['trashed'])) { if ($filters['trashed'] === 'only') { $query->onlyTrashed(); } } $sortBy = $filters['sort_by'] ?? 'sort_order'; $sortDirection = $filters['sort_direction'] ?? 'asc'; $query->orderBy($sortBy, $sortDirection)->orderBy('id', 'desc'); return $query->paginate($perPage); } public function getTimelineData(?string $phase = null): array { $query = AdminRoadmapPlan::query() ->whereIn('status', [ AdminRoadmapPlan::STATUS_PLANNED, AdminRoadmapPlan::STATUS_IN_PROGRESS, AdminRoadmapPlan::STATUS_COMPLETED, AdminRoadmapPlan::STATUS_DELAYED, ]) ->withCount('milestones') ->orderBy('phase') ->orderBy('sort_order') ->orderBy('start_date'); if ($phase) { $query->where('phase', $phase); } $plans = $query->get(); $grouped = []; foreach (AdminRoadmapPlan::getPhases() as $key => $label) { $phasePlans = $plans->where('phase', $key); if ($phase && $key !== $phase) { continue; } $grouped[$key] = [ 'label' => $label, 'plans' => $phasePlans->values(), ]; } return $grouped; } public function getStats(): array { return [ 'total' => AdminRoadmapPlan::count(), 'planned' => AdminRoadmapPlan::status(AdminRoadmapPlan::STATUS_PLANNED)->count(), 'in_progress' => AdminRoadmapPlan::status(AdminRoadmapPlan::STATUS_IN_PROGRESS)->count(), 'completed' => AdminRoadmapPlan::status(AdminRoadmapPlan::STATUS_COMPLETED)->count(), 'delayed' => AdminRoadmapPlan::status(AdminRoadmapPlan::STATUS_DELAYED)->count(), 'cancelled' => AdminRoadmapPlan::status(AdminRoadmapPlan::STATUS_CANCELLED)->count(), ]; } public function getDashboardSummary(): array { $stats = $this->getStats(); $timeline = $this->getTimelineData(); return [ 'stats' => $stats, 'timeline' => $timeline, ]; } public function getPlanById(int $id, bool $withTrashed = false): ?AdminRoadmapPlan { $query = AdminRoadmapPlan::query() ->with(['milestones' => function ($q) { $q->orderBy('sort_order')->orderBy('id'); }, 'milestones.assignee', 'creator', 'updater']) ->withCount('milestones'); if ($withTrashed) { $query->withTrashed(); } return $query->find($id); } public function createPlan(array $data): AdminRoadmapPlan { $data['created_by'] = auth()->id(); return AdminRoadmapPlan::create($data); } public function updatePlan(int $id, array $data): bool { $plan = AdminRoadmapPlan::findOrFail($id); $data['updated_by'] = auth()->id(); return $plan->update($data); } public function deletePlan(int $id): bool { $plan = AdminRoadmapPlan::findOrFail($id); $plan->deleted_by = auth()->id(); $plan->save(); return $plan->delete(); } public function restorePlan(int $id): bool { $plan = AdminRoadmapPlan::onlyTrashed()->findOrFail($id); $plan->deleted_by = null; return $plan->restore(); } public function changeStatus(int $id, string $status): AdminRoadmapPlan { $plan = AdminRoadmapPlan::findOrFail($id); $plan->status = $status; $plan->updated_by = auth()->id(); $plan->save(); return $plan; } public function recalculateProgress(int $planId): void { $plan = AdminRoadmapPlan::findOrFail($planId); $total = $plan->milestones()->count(); if ($total === 0) { return; } $completed = $plan->milestones() ->where('status', AdminRoadmapMilestone::STATUS_COMPLETED) ->count(); $plan->progress = (int) round(($completed / $total) * 100); $plan->save(); } }