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', 'is_curated', 'experience_level', 'job_category_id', 'job_location_id', 'fingerprint', ]; protected $casts = [ 'posted_at' => 'datetime', 'expires_at' => 'datetime', 'remote_allowed' => 'boolean', 'is_curated' => '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(); } /** * Get the tags for this job. */ public function tags(): BelongsToMany { return $this->belongsToMany(Tag::class); } /** * 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 uncurated jobs. */ public function scopeUncurated($query) { return $query->where('is_curated', false); } /** * 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'; } }