246 lines
6.4 KiB
PHP
246 lines
6.4 KiB
PHP
<?php
|
|
|
|
namespace App\Models;
|
|
|
|
use Illuminate\Database\Eloquent\Model;
|
|
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
|
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
|
|
use Illuminate\Database\Eloquent\SoftDeletes;
|
|
use Illuminate\Support\Str;
|
|
|
|
class Job extends Model
|
|
{
|
|
use SoftDeletes;
|
|
|
|
protected static function boot()
|
|
{
|
|
parent::boot();
|
|
|
|
static::creating(function ($job) {
|
|
if (empty($job->slug)) {
|
|
$job->slug = static::generateUniqueSlug($job->title, $job->company);
|
|
}
|
|
});
|
|
|
|
static::updating(function ($job) {
|
|
if ($job->isDirty('title') && empty($job->slug)) {
|
|
$job->slug = static::generateUniqueSlug($job->title, $job->company);
|
|
}
|
|
});
|
|
}
|
|
|
|
protected $fillable = [
|
|
'external_id',
|
|
'slug',
|
|
'title',
|
|
'company',
|
|
'description',
|
|
'location',
|
|
'salary_min',
|
|
'salary_max',
|
|
'salary_currency',
|
|
'job_type',
|
|
'job_source_id',
|
|
'source_url',
|
|
'apply_url',
|
|
'posted_at',
|
|
'expires_at',
|
|
'remote_allowed',
|
|
'experience_level',
|
|
'job_category_id',
|
|
'job_location_id',
|
|
'fingerprint',
|
|
];
|
|
|
|
protected $casts = [
|
|
'posted_at' => 'datetime',
|
|
'expires_at' => 'datetime',
|
|
'remote_allowed' => 'boolean',
|
|
'salary_min' => 'decimal:2',
|
|
'salary_max' => 'decimal:2',
|
|
];
|
|
|
|
/**
|
|
* Get the source that provides this job.
|
|
*/
|
|
public function source(): BelongsTo
|
|
{
|
|
return $this->belongsTo(JobSource::class, 'job_source_id');
|
|
}
|
|
|
|
/**
|
|
* Get the category that this job belongs to.
|
|
*/
|
|
public function category(): BelongsTo
|
|
{
|
|
return $this->belongsTo(JobCategory::class, 'job_category_id');
|
|
}
|
|
|
|
/**
|
|
* Get the location of this job.
|
|
*/
|
|
public function jobLocation(): BelongsTo
|
|
{
|
|
return $this->belongsTo(JobLocation::class, 'job_location_id');
|
|
}
|
|
|
|
/**
|
|
* Get the users who saved this job.
|
|
*/
|
|
public function savedByUsers(): BelongsToMany
|
|
{
|
|
return $this->belongsToMany(User::class, 'user_saved_jobs')
|
|
->withTimestamps();
|
|
}
|
|
|
|
/**
|
|
* Scope a query to only include active jobs.
|
|
*/
|
|
public function scopeActive($query)
|
|
{
|
|
return $query->where(function ($q) {
|
|
$q->whereNull('expires_at')
|
|
->orWhere('expires_at', '>', now());
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Scope a query to only include remote jobs.
|
|
*/
|
|
public function scopeRemote($query)
|
|
{
|
|
return $query->where('remote_allowed', true);
|
|
}
|
|
|
|
/**
|
|
* Scope a query to filter by job type.
|
|
*/
|
|
public function scopeOfType($query, string $type)
|
|
{
|
|
return $query->where('job_type', $type);
|
|
}
|
|
|
|
/**
|
|
* Scope a query to filter by experience level.
|
|
*/
|
|
public function scopeExperienceLevel($query, string $level)
|
|
{
|
|
return $query->where('experience_level', $level);
|
|
}
|
|
|
|
/**
|
|
* Scope a query to search by keywords in title, company, and description.
|
|
*/
|
|
public function scopeSearchKeywords($query, string $keywords)
|
|
{
|
|
return $query->where(function ($q) use ($keywords) {
|
|
$q->where('title', 'LIKE', "%{$keywords}%")
|
|
->orWhere('company', 'LIKE', "%{$keywords}%")
|
|
->orWhere('description', 'LIKE', "%{$keywords}%");
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Scope a query to filter by category.
|
|
*/
|
|
public function scopeInCategory($query, int $categoryId)
|
|
{
|
|
return $query->where('job_category_id', $categoryId);
|
|
}
|
|
|
|
/**
|
|
* Scope a query to filter by location.
|
|
*/
|
|
public function scopeInLocation($query, string $location)
|
|
{
|
|
return $query->where(function ($q) use ($location) {
|
|
$q->where('location', 'LIKE', "%{$location}%")
|
|
->orWhereHas('jobLocation', function ($q2) use ($location) {
|
|
$q2->where('city', 'LIKE', "%{$location}%")
|
|
->orWhere('state', 'LIKE', "%{$location}%")
|
|
->orWhere('country', 'LIKE', "%{$location}%");
|
|
});
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Scope a query to filter by salary range.
|
|
*/
|
|
public function scopeWithinSalaryRange($query, ?float $minSalary = null, ?float $maxSalary = null)
|
|
{
|
|
if ($minSalary) {
|
|
$query->where(function ($q) use ($minSalary) {
|
|
$q->where('salary_max', '>=', $minSalary)
|
|
->orWhereNull('salary_max');
|
|
});
|
|
}
|
|
|
|
if ($maxSalary) {
|
|
$query->where(function ($q) use ($maxSalary) {
|
|
$q->where('salary_min', '<=', $maxSalary)
|
|
->orWhereNull('salary_min');
|
|
});
|
|
}
|
|
|
|
return $query;
|
|
}
|
|
|
|
/**
|
|
* Scope a query to filter by posted date.
|
|
*/
|
|
public function scopePostedWithin($query, int $days)
|
|
{
|
|
return $query->where(function ($q) use ($days) {
|
|
$q->where('posted_at', '>=', now()->subDays($days))
|
|
->orWhere(function ($q2) use ($days) {
|
|
$q2->whereNull('posted_at')
|
|
->where('created_at', '>=', now()->subDays($days));
|
|
});
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Scope a query to filter by company.
|
|
*/
|
|
public function scopeByCompany($query, string $company)
|
|
{
|
|
return $query->where('company', 'LIKE', "%{$company}%");
|
|
}
|
|
|
|
/**
|
|
* Generate a unique slug for the job.
|
|
*/
|
|
public static function generateUniqueSlug(string $title, ?string $company = null): string
|
|
{
|
|
// Create base slug from title
|
|
$baseSlug = Str::slug($title);
|
|
|
|
// Add company to slug if provided for better uniqueness
|
|
if ($company) {
|
|
$baseSlug .= '-' . Str::slug(Str::limit($company, 30, ''));
|
|
}
|
|
|
|
// Limit slug length to avoid database issues
|
|
$baseSlug = Str::limit($baseSlug, 200, '');
|
|
|
|
$slug = $baseSlug;
|
|
$counter = 1;
|
|
|
|
// Check if slug exists and append counter if needed
|
|
while (static::withTrashed()->where('slug', $slug)->exists()) {
|
|
$slug = $baseSlug . '-' . $counter;
|
|
$counter++;
|
|
}
|
|
|
|
return $slug;
|
|
}
|
|
|
|
/**
|
|
* Get the route key for the model.
|
|
*/
|
|
public function getRouteKeyName(): string
|
|
{
|
|
return 'slug';
|
|
}
|
|
}
|