224 lines
7.3 KiB
PHP
224 lines
7.3 KiB
PHP
<?php
|
|||
|
|
|
||
|
|
namespace App\Services;
|
||
|
|
|
||
|
|
use App\Models\Job;
|
||
|
|
use Illuminate\Contracts\Pagination\LengthAwarePaginator;
|
||
|
|
use Illuminate\Database\Eloquent\Builder;
|
||
|
|
|
||
|
|
class JobSearchService
|
||
|
|
{
|
||
|
|
/**
|
||
|
|
* Search jobs with advanced filters
|
||
|
|
*/
|
||
|
|
public function search(array $filters = [], int $perPage = 15): LengthAwarePaginator
|
||
|
|
{
|
||
|
|
$query = Job::query()
|
||
|
|
->with(['source', 'category', 'jobLocation'])
|
||
|
|
->active();
|
||
|
|
|
||
|
|
// Apply filters
|
||
|
|
$this->applyFilters($query, $filters);
|
||
|
|
|
||
|
|
// Apply sorting
|
||
|
|
$this->applySorting($query, $filters['sort'] ?? 'date');
|
||
|
|
|
||
|
|
return $query->paginate($perPage);
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Apply filters to the query
|
||
|
|
*/
|
||
|
|
private function applyFilters(Builder $query, array $filters): void
|
||
|
|
{
|
||
|
|
// Keyword search (title, company, description)
|
||
|
|
if (!empty($filters['keywords'])) {
|
||
|
|
$keywords = $filters['keywords'];
|
||
|
|
$query->where(function ($q) use ($keywords) {
|
||
|
|
$q->where('title', 'LIKE', "%{$keywords}%")
|
||
|
|
->orWhere('company', 'LIKE', "%{$keywords}%")
|
||
|
|
->orWhere('description', 'LIKE', "%{$keywords}%");
|
||
|
|
});
|
||
|
|
}
|
||
|
|
|
||
|
|
// Full-text search (if supported)
|
||
|
|
if (!empty($filters['search'])) {
|
||
|
|
$search = $filters['search'];
|
||
|
|
$query->whereRaw(
|
||
|
|
"MATCH(title, company, description) AGAINST(? IN BOOLEAN MODE)",
|
||
|
|
[$search]
|
||
|
|
);
|
||
|
|
}
|
||
|
|
|
||
|
|
// Location filter
|
||
|
|
if (!empty($filters['location'])) {
|
||
|
|
$location = $filters['location'];
|
||
|
|
$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}%");
|
||
|
|
});
|
||
|
|
});
|
||
|
|
}
|
||
|
|
|
||
|
|
// Category filter
|
||
|
|
if (!empty($filters['category_id'])) {
|
||
|
|
$query->where('job_category_id', $filters['category_id']);
|
||
|
|
}
|
||
|
|
|
||
|
|
// Job type filter (can be array for multiple types)
|
||
|
|
if (!empty($filters['job_type'])) {
|
||
|
|
$types = is_array($filters['job_type']) ? $filters['job_type'] : [$filters['job_type']];
|
||
|
|
$query->whereIn('job_type', $types);
|
||
|
|
}
|
||
|
|
|
||
|
|
// Experience level filter (can be array)
|
||
|
|
if (!empty($filters['experience_level'])) {
|
||
|
|
$levels = is_array($filters['experience_level']) ? $filters['experience_level'] : [$filters['experience_level']];
|
||
|
|
$query->whereIn('experience_level', $levels);
|
||
|
|
}
|
||
|
|
|
||
|
|
// Remote filter
|
||
|
|
if (isset($filters['remote']) && $filters['remote'] !== '') {
|
||
|
|
$query->where('remote_allowed', (bool) $filters['remote']);
|
||
|
|
}
|
||
|
|
|
||
|
|
// Salary range filter
|
||
|
|
if (!empty($filters['salary_min'])) {
|
||
|
|
$query->where(function ($q) use ($filters) {
|
||
|
|
$q->where('salary_max', '>=', $filters['salary_min'])
|
||
|
|
->orWhereNull('salary_max');
|
||
|
|
});
|
||
|
|
}
|
||
|
|
|
||
|
|
if (!empty($filters['salary_max'])) {
|
||
|
|
$query->where(function ($q) use ($filters) {
|
||
|
|
$q->where('salary_min', '<=', $filters['salary_max'])
|
||
|
|
->orWhereNull('salary_min');
|
||
|
|
});
|
||
|
|
}
|
||
|
|
|
||
|
|
// Posted date filter (e.g., last 24 hours, 7 days, 30 days)
|
||
|
|
if (!empty($filters['posted_within'])) {
|
||
|
|
$days = (int) $filters['posted_within'];
|
||
|
|
$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));
|
||
|
|
});
|
||
|
|
});
|
||
|
|
}
|
||
|
|
|
||
|
|
// Source filter
|
||
|
|
if (!empty($filters['source_id'])) {
|
||
|
|
$query->where('job_source_id', $filters['source_id']);
|
||
|
|
}
|
||
|
|
|
||
|
|
// Company filter
|
||
|
|
if (!empty($filters['company'])) {
|
||
|
|
$query->where('company', 'LIKE', "%{$filters['company']}%");
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Apply sorting to the query
|
||
|
|
*/
|
||
|
|
private function applySorting(Builder $query, string $sort): void
|
||
|
|
{
|
||
|
|
match ($sort) {
|
||
|
|
'date' => $query->latest('posted_at'),
|
||
|
|
'date_asc' => $query->oldest('posted_at'),
|
||
|
|
'salary_desc' => $query->orderBy('salary_max', 'desc')->orderBy('salary_min', 'desc'),
|
||
|
|
'salary_asc' => $query->orderBy('salary_min', 'asc')->orderBy('salary_max', 'asc'),
|
||
|
|
'title' => $query->orderBy('title', 'asc'),
|
||
|
|
'company' => $query->orderBy('company', 'asc'),
|
||
|
|
'relevance' => $query->latest('created_at'), // Default to newest if no full-text score
|
||
|
|
default => $query->latest('posted_at'),
|
||
|
|
};
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Get autocomplete suggestions for job titles
|
||
|
|
*/
|
||
|
|
public function getTitleSuggestions(string $query, int $limit = 10): array
|
||
|
|
{
|
||
|
|
return Job::select('title')
|
||
|
|
->where('title', 'LIKE', "{$query}%")
|
||
|
|
->active()
|
||
|
|
->groupBy('title')
|
||
|
|
->limit($limit)
|
||
|
|
->pluck('title')
|
||
|
|
->toArray();
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Get autocomplete suggestions for companies
|
||
|
|
*/
|
||
|
|
public function getCompanySuggestions(string $query, int $limit = 10): array
|
||
|
|
{
|
||
|
|
return Job::select('company')
|
||
|
|
->where('company', 'LIKE', "{$query}%")
|
||
|
|
->active()
|
||
|
|
->groupBy('company')
|
||
|
|
->limit($limit)
|
||
|
|
->pluck('company')
|
||
|
|
->toArray();
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Get autocomplete suggestions for locations
|
||
|
|
*/
|
||
|
|
public function getLocationSuggestions(string $query, int $limit = 10): array
|
||
|
|
{
|
||
|
|
return Job::select('location')
|
||
|
|
->where('location', 'LIKE', "{$query}%")
|
||
|
|
->whereNotNull('location')
|
||
|
|
->active()
|
||
|
|
->groupBy('location')
|
||
|
|
->limit($limit)
|
||
|
|
->pluck('location')
|
||
|
|
->toArray();
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Get available filter options
|
||
|
|
*/
|
||
|
|
public function getFilterOptions(): array
|
||
|
|
{
|
||
|
|
return [
|
||
|
|
'job_types' => [
|
||
|
|
'full-time' => 'Full-time',
|
||
|
|
'part-time' => 'Part-time',
|
||
|
|
'contract' => 'Contract',
|
||
|
|
'temporary' => 'Temporary',
|
||
|
|
'internship' => 'Internship',
|
||
|
|
'freelance' => 'Freelance',
|
||
|
|
],
|
||
|
|
'experience_levels' => [
|
||
|
|
'entry' => 'Entry Level',
|
||
|
|
'mid' => 'Mid Level',
|
||
|
|
'senior' => 'Senior Level',
|
||
|
|
'lead' => 'Lead',
|
||
|
|
'executive' => 'Executive',
|
||
|
|
],
|
||
|
|
'posted_within' => [
|
||
|
|
1 => 'Last 24 hours',
|
||
|
|
7 => 'Last 7 days',
|
||
|
|
30 => 'Last 30 days',
|
||
|
|
90 => 'Last 3 months',
|
||
|
|
],
|
||
|
|
'sort_options' => [
|
||
|
|
'date' => 'Most Recent',
|
||
|
|
'date_asc' => 'Oldest First',
|
||
|
|
'salary_desc' => 'Highest Salary',
|
||
|
|
'salary_asc' => 'Lowest Salary',
|
||
|
|
'title' => 'Title A-Z',
|
||
|
|
'company' => 'Company A-Z',
|
||
|
|
],
|
||
|
|
];
|
||
|
|
}
|
||
|
|
}
|