261 lines
6.1 KiB
PHP
261 lines
6.1 KiB
PHP
<?php
|
|
|
|
/**
|
|
* Resume Model
|
|
* Professional Resume Builder - Resume Entity
|
|
*
|
|
* @author David Valera Melendez <david@valera-melendez.de>
|
|
* @created 2025-08-08
|
|
* @location Made in Germany 🇩🇪
|
|
*/
|
|
|
|
namespace App\Models;
|
|
|
|
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
|
use Illuminate\Database\Eloquent\Model;
|
|
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
|
use Illuminate\Database\Eloquent\SoftDeletes;
|
|
|
|
/**
|
|
* Resume Model
|
|
* Represents user resumes with all personal and professional information
|
|
*/
|
|
class Resume extends Model
|
|
{
|
|
use HasFactory, SoftDeletes;
|
|
|
|
/**
|
|
* The attributes that are mass assignable.
|
|
*
|
|
* @var array<int, string>
|
|
*/
|
|
protected $fillable = [
|
|
'user_id',
|
|
'title',
|
|
'template_id',
|
|
'slug',
|
|
'personal_info',
|
|
'professional_summary',
|
|
'work_experiences',
|
|
'education',
|
|
'skills',
|
|
'languages',
|
|
'certifications',
|
|
'projects',
|
|
'references',
|
|
'custom_sections',
|
|
'settings',
|
|
'is_active',
|
|
'is_completed',
|
|
'is_public',
|
|
'public_url',
|
|
'created_at',
|
|
'updated_at'
|
|
];
|
|
|
|
/**
|
|
* The attributes that should be cast.
|
|
*
|
|
* @var array<string, string>
|
|
*/
|
|
protected $casts = [
|
|
'personal_info' => 'array',
|
|
'work_experiences' => 'array',
|
|
'education' => 'array',
|
|
'skills' => 'array',
|
|
'languages' => 'array',
|
|
'certifications' => 'array',
|
|
'projects' => 'array',
|
|
'references' => 'array',
|
|
'custom_sections' => 'array',
|
|
'settings' => 'array',
|
|
'is_active' => 'boolean',
|
|
'is_completed' => 'boolean',
|
|
'is_public' => 'boolean',
|
|
];
|
|
|
|
/**
|
|
* Get the user that owns the resume.
|
|
*/
|
|
public function user(): BelongsTo
|
|
{
|
|
return $this->belongsTo(User::class);
|
|
}
|
|
|
|
/**
|
|
* Get the resume's public URL.
|
|
*/
|
|
public function getPublicUrlAttribute(): ?string
|
|
{
|
|
if ($this->is_public && $this->public_url) {
|
|
return route('public.resume', $this->public_url);
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
/**
|
|
* Get the resume's preview URL.
|
|
*/
|
|
public function getPreviewUrlAttribute(): string
|
|
{
|
|
return route('resume-builder.preview', $this);
|
|
}
|
|
|
|
/**
|
|
* Get the resume's edit URL.
|
|
*/
|
|
public function getEditUrlAttribute(): string
|
|
{
|
|
return route('resume-builder.edit', $this);
|
|
}
|
|
|
|
/**
|
|
* Get the resume's PDF download URL.
|
|
*/
|
|
public function getPdfUrlAttribute(): string
|
|
{
|
|
return route('resume-builder.download-pdf', $this);
|
|
}
|
|
|
|
/**
|
|
* Calculate completion percentage of the resume.
|
|
*/
|
|
public function getCompletionPercentageAttribute(): int
|
|
{
|
|
$sections = [
|
|
'personal_info' => 25,
|
|
'professional_summary' => 10,
|
|
'work_experiences' => 30,
|
|
'education' => 15,
|
|
'skills' => 10,
|
|
'languages' => 5,
|
|
'certifications' => 5,
|
|
];
|
|
|
|
$completed = 0;
|
|
foreach ($sections as $section => $weight) {
|
|
if ($this->isSectionCompleted($section)) {
|
|
$completed += $weight;
|
|
}
|
|
}
|
|
|
|
return min(100, $completed);
|
|
}
|
|
|
|
/**
|
|
* Check if a specific section is completed.
|
|
*/
|
|
public function isSectionCompleted(string $section): bool
|
|
{
|
|
$data = $this->$section;
|
|
|
|
if (empty($data)) {
|
|
return false;
|
|
}
|
|
|
|
switch ($section) {
|
|
case 'personal_info':
|
|
$required = ['first_name', 'last_name', 'email', 'phone'];
|
|
foreach ($required as $field) {
|
|
if (empty($data[$field] ?? null)) {
|
|
return false;
|
|
}
|
|
}
|
|
return true;
|
|
|
|
case 'professional_summary':
|
|
return !empty($data) && strlen($data) >= 50;
|
|
|
|
case 'work_experiences':
|
|
return is_array($data) && count($data) > 0 && !empty($data[0]['company'] ?? null);
|
|
|
|
case 'education':
|
|
return is_array($data) && count($data) > 0 && !empty($data[0]['institution'] ?? null);
|
|
|
|
case 'skills':
|
|
return is_array($data) && count($data) > 0;
|
|
|
|
default:
|
|
return !empty($data);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Generate a unique slug for the resume.
|
|
*/
|
|
public function generateSlug(): string
|
|
{
|
|
$baseSlug = str()->slug($this->title);
|
|
$slug = $baseSlug;
|
|
$counter = 1;
|
|
|
|
while (static::where('slug', $slug)->where('id', '!=', $this->id)->exists()) {
|
|
$slug = $baseSlug . '-' . $counter;
|
|
$counter++;
|
|
}
|
|
|
|
return $slug;
|
|
}
|
|
|
|
/**
|
|
* Generate a unique public URL for the resume.
|
|
*/
|
|
public function generatePublicUrl(): string
|
|
{
|
|
return str()->random(32);
|
|
}
|
|
|
|
/**
|
|
* Mark the resume as completed.
|
|
*/
|
|
public function markAsCompleted(): void
|
|
{
|
|
$this->update(['is_completed' => true]);
|
|
}
|
|
|
|
/**
|
|
* Scope a query to only include active resumes.
|
|
*/
|
|
public function scopeActive($query)
|
|
{
|
|
return $query->where('is_active', true);
|
|
}
|
|
|
|
/**
|
|
* Scope a query to only include completed resumes.
|
|
*/
|
|
public function scopeCompleted($query)
|
|
{
|
|
return $query->where('is_completed', true);
|
|
}
|
|
|
|
/**
|
|
* Scope a query to only include public resumes.
|
|
*/
|
|
public function scopePublic($query)
|
|
{
|
|
return $query->where('is_public', true);
|
|
}
|
|
|
|
/**
|
|
* Boot the model.
|
|
*/
|
|
protected static function boot()
|
|
{
|
|
parent::boot();
|
|
|
|
static::creating(function ($resume) {
|
|
if (empty($resume->slug)) {
|
|
$resume->slug = $resume->generateSlug();
|
|
}
|
|
});
|
|
|
|
static::updating(function ($resume) {
|
|
if ($resume->isDirty('title')) {
|
|
$resume->slug = $resume->generateSlug();
|
|
}
|
|
});
|
|
}
|
|
}
|