init commit
This commit is contained in:
380
app/Services/ResumeService.php
Normal file
380
app/Services/ResumeService.php
Normal file
@@ -0,0 +1,380 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* Resume Service
|
||||
* Professional Resume Builder - Resume Management Service
|
||||
*
|
||||
* @author David Valera Melendez <david@valera-melendez.de>
|
||||
* @created 2025-08-08
|
||||
* @location Made in Germany 🇩🇪
|
||||
*/
|
||||
|
||||
namespace App\Services;
|
||||
|
||||
use App\Models\Resume;
|
||||
use App\Models\User;
|
||||
use Illuminate\Database\Eloquent\Collection;
|
||||
use Illuminate\Http\Response;
|
||||
use Barryvdh\DomPDF\Facade\Pdf;
|
||||
|
||||
/**
|
||||
* Resume Service
|
||||
* Handles resume creation, management, and operations
|
||||
*/
|
||||
class ResumeService
|
||||
{
|
||||
/**
|
||||
* Available resume templates
|
||||
*/
|
||||
protected array $templates = [
|
||||
'professional' => [
|
||||
'id' => 'professional',
|
||||
'name' => 'Professional Classic',
|
||||
'description' => 'Clean and professional design perfect for corporate environments',
|
||||
'preview' => 'templates/professional-preview.jpg',
|
||||
'category' => 'Professional'
|
||||
],
|
||||
'modern' => [
|
||||
'id' => 'modern',
|
||||
'name' => 'Modern Creative',
|
||||
'description' => 'Contemporary design with creative elements for modern companies',
|
||||
'preview' => 'templates/modern-preview.jpg',
|
||||
'category' => 'Creative'
|
||||
],
|
||||
'executive' => [
|
||||
'id' => 'executive',
|
||||
'name' => 'Executive',
|
||||
'description' => 'Sophisticated layout for senior-level positions',
|
||||
'preview' => 'templates/executive-preview.jpg',
|
||||
'category' => 'Executive'
|
||||
],
|
||||
'minimal' => [
|
||||
'id' => 'minimal',
|
||||
'name' => 'Minimal Clean',
|
||||
'description' => 'Simple and clean design focusing on content',
|
||||
'preview' => 'templates/minimal-preview.jpg',
|
||||
'category' => 'Minimal'
|
||||
],
|
||||
'technical' => [
|
||||
'id' => 'technical',
|
||||
'name' => 'Technical',
|
||||
'description' => 'Optimized for technical and engineering roles',
|
||||
'preview' => 'templates/technical-preview.jpg',
|
||||
'category' => 'Technical'
|
||||
]
|
||||
];
|
||||
|
||||
/**
|
||||
* Get all resumes for a specific user
|
||||
*/
|
||||
public function getUserResumes(int $userId): Collection
|
||||
{
|
||||
return Resume::where('user_id', $userId)
|
||||
->orderBy('updated_at', 'desc')
|
||||
->get();
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new resume
|
||||
*/
|
||||
public function createResume(array $resumeData): Resume
|
||||
{
|
||||
// Set default values
|
||||
$resumeData['is_active'] = true;
|
||||
$resumeData['is_completed'] = false;
|
||||
$resumeData['is_public'] = false;
|
||||
|
||||
// Initialize empty sections
|
||||
$resumeData['personal_info'] = $resumeData['personal_info'] ?? [];
|
||||
$resumeData['work_experiences'] = $resumeData['work_experiences'] ?? [];
|
||||
$resumeData['education'] = $resumeData['education'] ?? [];
|
||||
$resumeData['skills'] = $resumeData['skills'] ?? [];
|
||||
$resumeData['languages'] = $resumeData['languages'] ?? [];
|
||||
$resumeData['certifications'] = $resumeData['certifications'] ?? [];
|
||||
$resumeData['projects'] = $resumeData['projects'] ?? [];
|
||||
$resumeData['references'] = $resumeData['references'] ?? [];
|
||||
$resumeData['custom_sections'] = $resumeData['custom_sections'] ?? [];
|
||||
$resumeData['settings'] = $resumeData['settings'] ?? $this->getDefaultSettings();
|
||||
|
||||
$resume = Resume::create($resumeData);
|
||||
|
||||
logger()->info('Resume created', [
|
||||
'resume_id' => $resume->id,
|
||||
'user_id' => $resume->user_id,
|
||||
'title' => $resume->title,
|
||||
]);
|
||||
|
||||
return $resume;
|
||||
}
|
||||
|
||||
/**
|
||||
* Update an existing resume
|
||||
*/
|
||||
public function updateResume(Resume $resume, array $resumeData): Resume
|
||||
{
|
||||
// Update completion status based on content
|
||||
if ($this->shouldMarkAsCompleted($resumeData)) {
|
||||
$resumeData['is_completed'] = true;
|
||||
}
|
||||
|
||||
$resume->update($resumeData);
|
||||
|
||||
logger()->info('Resume updated', [
|
||||
'resume_id' => $resume->id,
|
||||
'user_id' => $resume->user_id,
|
||||
'completion' => $resume->completion_percentage,
|
||||
]);
|
||||
|
||||
return $resume;
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete a resume
|
||||
*/
|
||||
public function deleteResume(Resume $resume): bool
|
||||
{
|
||||
$resumeId = $resume->id;
|
||||
$userId = $resume->user_id;
|
||||
|
||||
$deleted = $resume->delete();
|
||||
|
||||
if ($deleted) {
|
||||
logger()->info('Resume deleted', [
|
||||
'resume_id' => $resumeId,
|
||||
'user_id' => $userId,
|
||||
]);
|
||||
}
|
||||
|
||||
return $deleted;
|
||||
}
|
||||
|
||||
/**
|
||||
* Duplicate a resume
|
||||
*/
|
||||
public function duplicateResume(Resume $resume, string $newTitle): Resume
|
||||
{
|
||||
$resumeData = $resume->toArray();
|
||||
|
||||
// Remove unique identifiers
|
||||
unset($resumeData['id'], $resumeData['slug'], $resumeData['public_url']);
|
||||
|
||||
// Set new title and reset status
|
||||
$resumeData['title'] = $newTitle;
|
||||
$resumeData['is_public'] = false;
|
||||
$resumeData['created_at'] = now();
|
||||
$resumeData['updated_at'] = now();
|
||||
|
||||
$newResume = Resume::create($resumeData);
|
||||
|
||||
logger()->info('Resume duplicated', [
|
||||
'original_resume_id' => $resume->id,
|
||||
'new_resume_id' => $newResume->id,
|
||||
'user_id' => $resume->user_id,
|
||||
]);
|
||||
|
||||
return $newResume;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate PDF for a resume
|
||||
*/
|
||||
public function generatePdf(Resume $resume): Response
|
||||
{
|
||||
$template = $this->getTemplate($resume->template_id ?? 'professional');
|
||||
|
||||
$pdf = Pdf::loadView('pdf.resume.' . $template['id'], [
|
||||
'resume' => $resume,
|
||||
'template' => $template
|
||||
]);
|
||||
|
||||
$fileName = str()->slug($resume->title) . '-resume.pdf';
|
||||
|
||||
logger()->info('PDF generated', [
|
||||
'resume_id' => $resume->id,
|
||||
'user_id' => $resume->user_id,
|
||||
'template' => $template['id'],
|
||||
]);
|
||||
|
||||
return $pdf->download($fileName);
|
||||
}
|
||||
|
||||
/**
|
||||
* Make resume public
|
||||
*/
|
||||
public function makePublic(Resume $resume): Resume
|
||||
{
|
||||
if (!$resume->public_url) {
|
||||
$resume->public_url = $resume->generatePublicUrl();
|
||||
}
|
||||
|
||||
$resume->update(['is_public' => true]);
|
||||
|
||||
logger()->info('Resume made public', [
|
||||
'resume_id' => $resume->id,
|
||||
'user_id' => $resume->user_id,
|
||||
'public_url' => $resume->public_url,
|
||||
]);
|
||||
|
||||
return $resume;
|
||||
}
|
||||
|
||||
/**
|
||||
* Make resume private
|
||||
*/
|
||||
public function makePrivate(Resume $resume): Resume
|
||||
{
|
||||
$resume->update(['is_public' => false]);
|
||||
|
||||
logger()->info('Resume made private', [
|
||||
'resume_id' => $resume->id,
|
||||
'user_id' => $resume->user_id,
|
||||
]);
|
||||
|
||||
return $resume;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get available templates
|
||||
*/
|
||||
public function getAvailableTemplates(): array
|
||||
{
|
||||
return $this->templates;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get specific template
|
||||
*/
|
||||
public function getTemplate(string $templateId): array
|
||||
{
|
||||
return $this->templates[$templateId] ?? $this->templates['professional'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get resume analytics
|
||||
*/
|
||||
public function getResumeAnalytics(Resume $resume): array
|
||||
{
|
||||
return [
|
||||
'completion_percentage' => $resume->completion_percentage,
|
||||
'sections_completed' => $this->getCompletedSections($resume),
|
||||
'total_sections' => 7,
|
||||
'word_count' => $this->calculateWordCount($resume),
|
||||
'last_updated' => $resume->updated_at,
|
||||
'views' => 0, // Implement view tracking if needed
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get completed sections count
|
||||
*/
|
||||
protected function getCompletedSections(Resume $resume): int
|
||||
{
|
||||
$sections = ['personal_info', 'professional_summary', 'work_experiences',
|
||||
'education', 'skills', 'languages', 'certifications'];
|
||||
|
||||
$completed = 0;
|
||||
foreach ($sections as $section) {
|
||||
if ($resume->isSectionCompleted($section)) {
|
||||
$completed++;
|
||||
}
|
||||
}
|
||||
|
||||
return $completed;
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate total word count in resume
|
||||
*/
|
||||
protected function calculateWordCount(Resume $resume): int
|
||||
{
|
||||
$wordCount = 0;
|
||||
|
||||
// Professional summary
|
||||
if ($resume->professional_summary) {
|
||||
$wordCount += str_word_count(strip_tags($resume->professional_summary));
|
||||
}
|
||||
|
||||
// Work experiences
|
||||
if ($resume->work_experiences) {
|
||||
foreach ($resume->work_experiences as $experience) {
|
||||
$wordCount += str_word_count(strip_tags($experience['description'] ?? ''));
|
||||
}
|
||||
}
|
||||
|
||||
// Education
|
||||
if ($resume->education) {
|
||||
foreach ($resume->education as $education) {
|
||||
$wordCount += str_word_count(strip_tags($education['description'] ?? ''));
|
||||
}
|
||||
}
|
||||
|
||||
// Projects
|
||||
if ($resume->projects) {
|
||||
foreach ($resume->projects as $project) {
|
||||
$wordCount += str_word_count(strip_tags($project['description'] ?? ''));
|
||||
}
|
||||
}
|
||||
|
||||
return $wordCount;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if resume should be marked as completed
|
||||
*/
|
||||
protected function shouldMarkAsCompleted(array $resumeData): bool
|
||||
{
|
||||
$requiredSections = ['personal_info', 'work_experiences', 'education'];
|
||||
|
||||
foreach ($requiredSections as $section) {
|
||||
if (empty($resumeData[$section])) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get default resume settings
|
||||
*/
|
||||
protected function getDefaultSettings(): array
|
||||
{
|
||||
return [
|
||||
'theme' => 'light',
|
||||
'font_family' => 'Roboto',
|
||||
'font_size' => 14,
|
||||
'color_scheme' => 'blue',
|
||||
'show_photo' => false,
|
||||
'show_references' => true,
|
||||
'show_certifications' => true,
|
||||
'show_projects' => true,
|
||||
'section_order' => [
|
||||
'personal_info',
|
||||
'professional_summary',
|
||||
'work_experiences',
|
||||
'education',
|
||||
'skills',
|
||||
'languages',
|
||||
'certifications',
|
||||
'projects',
|
||||
'references'
|
||||
]
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get resume statistics for user
|
||||
*/
|
||||
public function getUserResumeStats(int $userId): array
|
||||
{
|
||||
$resumes = $this->getUserResumes($userId);
|
||||
|
||||
return [
|
||||
'total_resumes' => $resumes->count(),
|
||||
'completed_resumes' => $resumes->where('is_completed', true)->count(),
|
||||
'public_resumes' => $resumes->where('is_public', true)->count(),
|
||||
'avg_completion' => $resumes->avg('completion_percentage') ?? 0,
|
||||
'last_updated' => $resumes->max('updated_at'),
|
||||
];
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user