init commit

This commit is contained in:
David Melendez
2026-01-14 22:38:44 +01:00
parent 4e0c415f0b
commit e25d53d054
124 changed files with 21653 additions and 1 deletions

View File

@@ -0,0 +1,522 @@
<?php
namespace App\Repositories;
use App\Interfaces\ResumeRepositoryInterface;
use App\Models\Resume;
use App\Models\User;
use Illuminate\Database\Eloquent\Collection;
use Illuminate\Contracts\Pagination\LengthAwarePaginator;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Str;
use Carbon\Carbon;
/**
* Resume Repository Implementation
*
* Handles all resume-related data operations.
* Implements ResumeRepositoryInterface with resume-specific business logic.
*
* @author David Valera Melendez <david@valera-melendez.de>
* @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;
}
}