* @since February 2025 */ class ResumeRepository extends BaseRepository implements ResumeRepositoryInterface { /** * Create a new ResumeRepository instance * * @param Resume $model */ public function __construct(Resume $model) { parent::__construct($model); } /** * Get user's resumes * * @param int $userId * @param array $columns * @return Collection */ public function getUserResumes(int $userId, array $columns = ['*']): Collection { return $this->orderBy('updated_at', 'desc') ->findBy(['user_id' => $userId], $columns); } /** * Get user's resumes with pagination * * @param int $userId * @param int $perPage * @return LengthAwarePaginator */ public function getPaginatedUserResumes(int $userId, int $perPage = 10): LengthAwarePaginator { $result = $this->query ->where('user_id', $userId) ->orderBy('updated_at', 'desc') ->paginate($perPage); $this->resetQuery(); return $result; } /** * Find resume by public URL * * @param string $publicUrl * @return Resume|null */ public function findByPublicUrl(string $publicUrl): ?Resume { return $this->findOneBy([ 'public_url' => $publicUrl, 'is_public' => true, 'status' => 'published' ]); } /** * Get public resumes * * @param int $limit * @return Collection */ public function getPublicResumes(int $limit = 20): Collection { $result = $this->query ->where('is_public', true) ->where('status', 'published') ->whereNull('expires_at') ->orWhere('expires_at', '>', Carbon::now()) ->orderBy('published_at', 'desc') ->limit($limit) ->with(['user:id,first_name,last_name']) ->get(); $this->resetQuery(); return $result; } /** * Get resumes by template * * @param string $template * @return Collection */ public function getResumesByTemplate(string $template): Collection { return $this->orderBy('created_at', 'desc') ->findBy(['template' => $template]); } /** * Get resumes by status * * @param string $status * @return Collection */ public function getResumesByStatus(string $status): Collection { return $this->orderBy('updated_at', 'desc') ->findBy(['status' => $status]); } /** * Get popular resumes (most viewed) * * @param int $limit * @param int $days * @return Collection */ public function getPopularResumes(int $limit = 10, int $days = 30): Collection { $cutoffDate = Carbon::now()->subDays($days); $result = $this->query ->where('is_public', true) ->where('status', 'published') ->where('last_viewed_at', '>=', $cutoffDate) ->orderBy('view_count', 'desc') ->limit($limit) ->with(['user:id,first_name,last_name']) ->get(); $this->resetQuery(); return $result; } /** * Get recent resumes * * @param int $limit * @param int $days * @return Collection */ public function getRecentResumes(int $limit = 10, int $days = 7): Collection { $cutoffDate = Carbon::now()->subDays($days); $result = $this->query ->where('is_public', true) ->where('status', 'published') ->where('published_at', '>=', $cutoffDate) ->orderBy('published_at', 'desc') ->limit($limit) ->with(['user:id,first_name,last_name']) ->get(); $this->resetQuery(); return $result; } /** * Search resumes * * @param string $query * @param array $filters * @param int $limit * @return Collection */ public function searchResumes(string $query, array $filters = [], int $limit = 20): Collection { $searchTerm = '%' . $query . '%'; $queryBuilder = $this->query ->where('is_public', true) ->where('status', 'published') ->where(function ($q) use ($searchTerm) { $q->where('title', 'like', $searchTerm) ->orWhere('description', 'like', $searchTerm) ->orWhereJsonContains('content->summary', $searchTerm) ->orWhereJsonLike('content->skills', $searchTerm); }); // Apply filters if (isset($filters['template']) && !empty($filters['template'])) { $queryBuilder = $queryBuilder->where('template', $filters['template']); } if (isset($filters['min_completion']) && is_numeric($filters['min_completion'])) { $queryBuilder = $queryBuilder->where('completion_percentage', '>=', $filters['min_completion']); } $result = $queryBuilder ->orderBy('view_count', 'desc') ->limit($limit) ->with(['user:id,first_name,last_name']) ->get(); $this->resetQuery(); return $result; } /** * Get incomplete resumes * * @param int $threshold * @return Collection */ public function getIncompleteResumes(int $threshold = 50): Collection { $result = $this->query ->where('completion_percentage', '<', $threshold) ->where('status', 'draft') ->orderBy('updated_at', 'desc') ->with(['user:id,first_name,last_name,email']) ->get(); $this->resetQuery(); return $result; } /** * Duplicate resume for user * * @param int $resumeId * @param int $userId * @param string $newTitle * @return Resume */ public function duplicateResume(int $resumeId, int $userId, string $newTitle): Resume { return $this->transaction(function () use ($resumeId, $userId, $newTitle) { $originalResume = $this->findOrFail($resumeId); $duplicateData = $originalResume->toArray(); // Remove unique fields and reset for new resume unset($duplicateData['id'], $duplicateData['created_at'], $duplicateData['updated_at'], $duplicateData['deleted_at']); $duplicateData['user_id'] = $userId; $duplicateData['title'] = $newTitle; $duplicateData['status'] = 'draft'; $duplicateData['is_public'] = false; $duplicateData['public_url'] = null; $duplicateData['published_at'] = null; $duplicateData['view_count'] = 0; $duplicateData['download_count'] = 0; $duplicateData['last_viewed_at'] = null; $duplicateData['last_downloaded_at'] = null; $duplicateData['pdf_path'] = null; $duplicateData['pdf_generated_at'] = null; $duplicateData['pdf_version'] = 1; return $this->create($duplicateData); }); } /** * Update resume view count * * @param int $resumeId * @return bool */ public function incrementViewCount(int $resumeId): bool { $resume = $this->find($resumeId); if (!$resume) { return false; } return $resume->update([ 'view_count' => $resume->view_count + 1, 'last_viewed_at' => $this->freshTimestamp(), ]); } /** * Update resume download count * * @param int $resumeId * @return bool */ public function incrementDownloadCount(int $resumeId): bool { $resume = $this->find($resumeId); if (!$resume) { return false; } return $resume->update([ 'download_count' => $resume->download_count + 1, 'last_downloaded_at' => $this->freshTimestamp(), ]); } /** * Get resume statistics * * @param int|null $userId * @return array */ public function getResumeStatistics(?int $userId = null): array { $query = $this->query; if ($userId) { $query = $query->where('user_id', $userId); } $stats = [ 'total_resumes' => $query->count(), 'draft_resumes' => $query->where('status', 'draft')->count(), 'published_resumes' => $query->where('status', 'published')->count(), 'archived_resumes' => $query->where('status', 'archived')->count(), 'public_resumes' => $query->where('is_public', true)->count(), 'private_resumes' => $query->where('is_public', false)->count(), ]; // Template statistics $templateStats = $query->select('template', DB::raw('count(*) as count')) ->groupBy('template') ->pluck('count', 'template') ->toArray(); $stats['by_template'] = $templateStats; // Completion statistics $stats['avg_completion'] = $query->avg('completion_percentage') ?? 0; $stats['completed_resumes'] = $query->where('completion_percentage', '>=', 90)->count(); // View statistics $stats['total_views'] = $query->sum('view_count'); $stats['total_downloads'] = $query->sum('download_count'); // Recent activity (last 30 days) $thirtyDaysAgo = Carbon::now()->subDays(30); $stats['created_last_30_days'] = $query->where('created_at', '>=', $thirtyDaysAgo)->count(); $stats['updated_last_30_days'] = $query->where('updated_at', '>=', $thirtyDaysAgo)->count(); $this->resetQuery(); return $stats; } /** * Get resumes requiring PDF regeneration * * @return Collection */ public function getResumesNeedingPdfRegeneration(): Collection { $result = $this->query ->where('status', 'published') ->where(function ($q) { $q->whereNull('pdf_generated_at') ->orWhere('updated_at', '>', DB::raw('pdf_generated_at')); }) ->orderBy('updated_at', 'desc') ->get(); $this->resetQuery(); return $result; } /** * Archive old resumes * * @param int $days * @return int Number of archived resumes */ public function archiveOldResumes(int $days = 365): int { $cutoffDate = Carbon::now()->subDays($days); $result = $this->query ->where('status', 'draft') ->where('updated_at', '<', $cutoffDate) ->update(['status' => 'archived']); $this->resetQuery(); return $result; } /** * Get user's resume by title * * @param int $userId * @param string $title * @return Resume|null */ public function getUserResumeByTitle(int $userId, string $title): ?Resume { return $this->findOneBy([ 'user_id' => $userId, 'title' => $title ]); } /** * Check if user can create more resumes * * @param int $userId * @param int $limit * @return bool */ public function canUserCreateMoreResumes(int $userId, int $limit = 10): bool { $userResumeCount = $this->count(['user_id' => $userId]); return $userResumeCount < $limit; } /** * Get featured resumes for showcase * * @param int $limit * @return Collection */ public function getFeaturedResumes(int $limit = 6): Collection { $result = $this->query ->where('is_public', true) ->where('status', 'published') ->where('completion_percentage', '>=', 90) ->where('view_count', '>', 50) ->orderBy('view_count', 'desc') ->orderBy('download_count', 'desc') ->limit($limit) ->with(['user:id,first_name,last_name']) ->get(); $this->resetQuery(); return $result; } /** * Update resume completion percentage * * @param int $resumeId * @return bool */ public function updateCompletionPercentage(int $resumeId): bool { $resume = $this->find($resumeId); if (!$resume) { return false; } $content = $resume->content ?? []; $sections = ['personal_info', 'summary', 'experience', 'education', 'skills']; $completedSections = 0; $totalSections = count($sections); foreach ($sections as $section) { if (isset($content[$section]) && !empty($content[$section])) { $completedSections++; } } $percentage = $totalSections > 0 ? intval(($completedSections / $totalSections) * 100) : 0; return $resume->update([ 'completion_percentage' => $percentage, 'completion_sections' => array_fill_keys($sections, false) ]); } /** * Get resumes expiring soon * * @param int $days * @return Collection */ public function getResumesExpiringSoon(int $days = 7): Collection { $futureDate = Carbon::now()->addDays($days); $result = $this->query ->where('is_public', true) ->whereNotNull('expires_at') ->whereBetween('expires_at', [Carbon::now(), $futureDate]) ->orderBy('expires_at', 'asc') ->with(['user:id,first_name,last_name,email']) ->get(); $this->resetQuery(); return $result; } /** * Bulk update resume status * * @param array $resumeIds * @param string $status * @return int Number of updated resumes */ public function bulkUpdateStatus(array $resumeIds, string $status): int { $result = $this->query ->whereIn('id', $resumeIds) ->update([ 'status' => $status, 'updated_at' => $this->freshTimestamp() ]); $this->resetQuery(); return $result; } }