init commit
This commit is contained in:
260
app/Models/Resume.php
Normal file
260
app/Models/Resume.php
Normal file
@@ -0,0 +1,260 @@
|
||||
<?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();
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
191
app/Models/User.php
Normal file
191
app/Models/User.php
Normal file
@@ -0,0 +1,191 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* User Model
|
||||
* Professional Resume Builder - User Entity
|
||||
*
|
||||
* @author David Valera Melendez <david@valera-melendez.de>
|
||||
* @created 2025-08-08
|
||||
* @location Made in Germany 🇩🇪
|
||||
*/
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Contracts\Auth\MustVerifyEmail;
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Relations\HasMany;
|
||||
use Illuminate\Foundation\Auth\User as Authenticatable;
|
||||
use Illuminate\Notifications\Notifiable;
|
||||
use Laravel\Sanctum\HasApiTokens;
|
||||
|
||||
/**
|
||||
* User Model
|
||||
* Represents application users with authentication and profile features
|
||||
*/
|
||||
class User extends Authenticatable implements MustVerifyEmail
|
||||
{
|
||||
use HasApiTokens, HasFactory, Notifiable;
|
||||
|
||||
/**
|
||||
* The attributes that are mass assignable.
|
||||
*
|
||||
* @var array<int, string>
|
||||
*/
|
||||
protected $fillable = [
|
||||
'first_name',
|
||||
'last_name',
|
||||
'email',
|
||||
'password',
|
||||
'phone',
|
||||
'date_of_birth',
|
||||
'gender',
|
||||
'avatar',
|
||||
'bio',
|
||||
'website',
|
||||
'linkedin',
|
||||
'github',
|
||||
'twitter',
|
||||
'preferences',
|
||||
'newsletter_subscribed',
|
||||
'last_login_at',
|
||||
'last_login_ip',
|
||||
'status',
|
||||
'suspended_at',
|
||||
'suspension_reason',
|
||||
'login_attempts',
|
||||
'locked_until',
|
||||
'locale',
|
||||
'timezone',
|
||||
'two_factor_enabled',
|
||||
'two_factor_secret',
|
||||
'two_factor_recovery_codes',
|
||||
'profile_completed_at'
|
||||
];
|
||||
|
||||
/**
|
||||
* The attributes that should be hidden for serialization.
|
||||
*
|
||||
* @var array<int, string>
|
||||
*/
|
||||
protected $hidden = [
|
||||
'password',
|
||||
'remember_token',
|
||||
];
|
||||
|
||||
/**
|
||||
* Get the attributes that should be cast.
|
||||
*
|
||||
* @return array<string, string>
|
||||
*/
|
||||
protected function casts(): array
|
||||
{
|
||||
return [
|
||||
'email_verified_at' => 'datetime',
|
||||
'last_login_at' => 'datetime',
|
||||
'date_of_birth' => 'date',
|
||||
'suspended_at' => 'datetime',
|
||||
'locked_until' => 'datetime',
|
||||
'profile_completed_at' => 'datetime',
|
||||
'newsletter_subscribed' => 'boolean',
|
||||
'two_factor_enabled' => 'boolean',
|
||||
'preferences' => 'array',
|
||||
'two_factor_recovery_codes' => 'array',
|
||||
'password' => 'hashed',
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the user's full name.
|
||||
*/
|
||||
public function getFullNameAttribute(): string
|
||||
{
|
||||
return "{$this->first_name} {$this->last_name}";
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the user's initials.
|
||||
*/
|
||||
public function getInitialsAttribute(): string
|
||||
{
|
||||
return strtoupper(substr($this->first_name, 0, 1) . substr($this->last_name, 0, 1));
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the user's avatar URL.
|
||||
*/
|
||||
public function getAvatarUrlAttribute(): string
|
||||
{
|
||||
if ($this->avatar) {
|
||||
return asset('storage/avatars/' . $this->avatar);
|
||||
}
|
||||
|
||||
return "https://ui-avatars.com/api/?name={$this->initials}&background=667eea&color=fff&size=200";
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if user has completed their profile.
|
||||
*/
|
||||
public function hasCompletedProfile(): bool
|
||||
{
|
||||
$requiredFields = ['first_name', 'last_name', 'email', 'phone', 'city', 'country'];
|
||||
|
||||
foreach ($requiredFields as $field) {
|
||||
if (empty($this->$field)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the user's resumes.
|
||||
*/
|
||||
public function resumes(): HasMany
|
||||
{
|
||||
return $this->hasMany(Resume::class)->orderBy('updated_at', 'desc');
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the user's active resumes.
|
||||
*/
|
||||
public function activeResumes(): HasMany
|
||||
{
|
||||
return $this->resumes()->where('is_active', true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the user's completed resumes.
|
||||
*/
|
||||
public function completedResumes(): HasMany
|
||||
{
|
||||
return $this->resumes()->where('is_completed', true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Update user's last login information.
|
||||
*/
|
||||
public function updateLastLogin(string $ipAddress): void
|
||||
{
|
||||
$this->update([
|
||||
'last_login_at' => now(),
|
||||
'last_login_ip' => $ipAddress,
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Scope a query to only include active users.
|
||||
*/
|
||||
public function scopeActive($query)
|
||||
{
|
||||
return $query->where('is_active', true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Scope a query to only include verified users.
|
||||
*/
|
||||
public function scopeVerified($query)
|
||||
{
|
||||
return $query->whereNotNull('email_verified_at');
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user