job application flow fixed
This commit is contained in:
parent
88e4f3fdb4
commit
93ffc13af5
@ -21,7 +21,7 @@ public function listJobs(Request $request)
|
|||||||
/** @var Worker $worker */
|
/** @var Worker $worker */
|
||||||
$worker = $request->attributes->get('worker');
|
$worker = $request->attributes->get('worker');
|
||||||
|
|
||||||
$query = JobPost::where('status', 'active')
|
$query = JobPost::whereIn('status', ['active', 'closed'])
|
||||||
->with(['employer.employerProfile', 'skills']);
|
->with(['employer.employerProfile', 'skills']);
|
||||||
|
|
||||||
if ($worker) {
|
if ($worker) {
|
||||||
@ -42,9 +42,85 @@ public function listJobs(Request $request)
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
$jobs = $query->latest()
|
$workerApplications = [];
|
||||||
->get()
|
if ($worker) {
|
||||||
->map(function ($job) {
|
$workerApplications = JobApplication::where('worker_id', $worker->id)
|
||||||
|
->pluck('status', 'job_id')
|
||||||
|
->toArray();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Apply filters
|
||||||
|
// Filter by job_type / jobType
|
||||||
|
if ($request->has('job_type') && $request->input('job_type') !== '') {
|
||||||
|
$query->where('job_type', $request->input('job_type'));
|
||||||
|
}
|
||||||
|
if ($request->has('jobType') && $request->input('jobType') !== '') {
|
||||||
|
$query->where('job_type', $request->input('jobType'));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Filter by skills
|
||||||
|
if ($request->has('skills') && $request->input('skills') !== '') {
|
||||||
|
$skills = $request->input('skills');
|
||||||
|
if (is_string($skills)) {
|
||||||
|
$skills = array_filter(array_map('trim', explode(',', $skills)));
|
||||||
|
}
|
||||||
|
if (!empty($skills)) {
|
||||||
|
$query->whereHas('skills', function ($q) use ($skills) {
|
||||||
|
$q->where(function($q2) use ($skills) {
|
||||||
|
$q2->whereIn('skills.id', $skills)
|
||||||
|
->orWhereIn('skills.name', $skills);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Filter by salary
|
||||||
|
if ($request->has('salary_min') && $request->input('salary_min') !== '') {
|
||||||
|
$query->where('salary', '>=', (float) $request->input('salary_min'));
|
||||||
|
}
|
||||||
|
if ($request->has('salary_max') && $request->input('salary_max') !== '') {
|
||||||
|
$query->where('salary', '<=', (float) $request->input('salary_max'));
|
||||||
|
}
|
||||||
|
if ($request->has('salary') && $request->input('salary') !== '') {
|
||||||
|
$query->where('salary', '>=', (float) $request->input('salary'));
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($request->has('search') && $request->input('search') !== '') {
|
||||||
|
$search = $request->input('search');
|
||||||
|
$query->where(function($q) use ($search) {
|
||||||
|
$q->where('title', 'like', "%{$search}%")
|
||||||
|
->orWhere('description', 'like', "%{$search}%")
|
||||||
|
->orWhere('location', 'like', "%{$search}%");
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Apply Sorting
|
||||||
|
if ($request->has('sortBy') && $request->input('sortBy') !== '') {
|
||||||
|
$sortBy = strtoupper($request->input('sortBy'));
|
||||||
|
if ($sortBy === 'LATEST FIRST') {
|
||||||
|
$query->latest();
|
||||||
|
} elseif ($sortBy === 'OLDEST FIRST') {
|
||||||
|
$query->oldest();
|
||||||
|
} elseif ($sortBy === 'PRICE: LOW TO HIGH') {
|
||||||
|
$query->orderBy('salary', 'asc');
|
||||||
|
} elseif ($sortBy === 'PRICE: HIGH TO LOW') {
|
||||||
|
$query->orderBy('salary', 'desc');
|
||||||
|
} else {
|
||||||
|
$query->latest();
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
$query->latest();
|
||||||
|
}
|
||||||
|
|
||||||
|
$jobs = $query->get()
|
||||||
|
->map(function ($job) use ($worker, $workerApplications) {
|
||||||
|
$status = 'new';
|
||||||
|
if ($worker && isset($workerApplications[$job->id])) {
|
||||||
|
$status = $workerApplications[$job->id];
|
||||||
|
} elseif ($job->status === 'closed') {
|
||||||
|
$status = 'closed';
|
||||||
|
}
|
||||||
|
|
||||||
return [
|
return [
|
||||||
'id' => $job->id,
|
'id' => $job->id,
|
||||||
'title' => $job->title,
|
'title' => $job->title,
|
||||||
@ -60,6 +136,7 @@ public function listJobs(Request $request)
|
|||||||
'company_name' => $job->employer->employerProfile->company_name ?? 'Private Sponsor',
|
'company_name' => $job->employer->employerProfile->company_name ?? 'Private Sponsor',
|
||||||
'posted_by' => $job->employer->name ?? 'Private Sponsor',
|
'posted_by' => $job->employer->name ?? 'Private Sponsor',
|
||||||
'posted_at' => $job->created_at->toIso8601String(),
|
'posted_at' => $job->created_at->toIso8601String(),
|
||||||
|
'status' => $status,
|
||||||
];
|
];
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -77,7 +154,7 @@ public function listJobs(Request $request)
|
|||||||
*/
|
*/
|
||||||
public function jobDetails($id)
|
public function jobDetails($id)
|
||||||
{
|
{
|
||||||
$job = JobPost::where('status', 'active')
|
$job = JobPost::whereIn('status', ['active', 'closed'])
|
||||||
->with(['employer.employerProfile', 'skills'])
|
->with(['employer.employerProfile', 'skills'])
|
||||||
->find($id);
|
->find($id);
|
||||||
|
|
||||||
@ -88,6 +165,21 @@ public function jobDetails($id)
|
|||||||
], 404);
|
], 404);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
$worker = request()->attributes->get('worker');
|
||||||
|
$status = 'new';
|
||||||
|
if ($worker) {
|
||||||
|
$application = JobApplication::where('worker_id', $worker->id)
|
||||||
|
->where('job_id', $job->id)
|
||||||
|
->first();
|
||||||
|
if ($application) {
|
||||||
|
$status = $application->status;
|
||||||
|
} elseif ($job->status === 'closed') {
|
||||||
|
$status = 'closed';
|
||||||
|
}
|
||||||
|
} elseif ($job->status === 'closed') {
|
||||||
|
$status = 'closed';
|
||||||
|
}
|
||||||
|
|
||||||
return response()->json([
|
return response()->json([
|
||||||
'success' => true,
|
'success' => true,
|
||||||
'data' => [
|
'data' => [
|
||||||
@ -106,6 +198,7 @@ public function jobDetails($id)
|
|||||||
'company_name' => $job->employer->employerProfile->company_name ?? 'Private Sponsor',
|
'company_name' => $job->employer->employerProfile->company_name ?? 'Private Sponsor',
|
||||||
'posted_by' => $job->employer->name ?? 'Private Sponsor',
|
'posted_by' => $job->employer->name ?? 'Private Sponsor',
|
||||||
'posted_at' => $job->created_at->toIso8601String(),
|
'posted_at' => $job->created_at->toIso8601String(),
|
||||||
|
'status' => $status,
|
||||||
]
|
]
|
||||||
]
|
]
|
||||||
]);
|
]);
|
||||||
@ -187,6 +280,7 @@ public function appliedJobs(Request $request)
|
|||||||
return [
|
return [
|
||||||
'application_id' => $app->id,
|
'application_id' => $app->id,
|
||||||
'status' => $app->status,
|
'status' => $app->status,
|
||||||
|
'employer_status' => $app->employer_status,
|
||||||
'applied_at' => $app->created_at->toIso8601String(),
|
'applied_at' => $app->created_at->toIso8601String(),
|
||||||
'job' => $job ? [
|
'job' => $job ? [
|
||||||
'id' => $job->id,
|
'id' => $job->id,
|
||||||
@ -660,7 +754,7 @@ public function employerUpdateApplicationStatus(Request $request, $id)
|
|||||||
}
|
}
|
||||||
|
|
||||||
$validator = Validator::make($request->all(), [
|
$validator = Validator::make($request->all(), [
|
||||||
'status' => 'required|string|in:applied,shortlisted,contacted,interview scheduled,interview_scheduled,selected,rejected,hired,hire_requested',
|
'status' => 'required|string|in:applied,review,reviewing,shortlisted,contacted,interview scheduled,interview_scheduled,selected,rejected,hired,hire_requested',
|
||||||
'notes' => 'nullable|string',
|
'notes' => 'nullable|string',
|
||||||
]);
|
]);
|
||||||
|
|
||||||
@ -687,6 +781,8 @@ public function employerUpdateApplicationStatus(Request $request, $id)
|
|||||||
$dbStatus = strtolower($request->status);
|
$dbStatus = strtolower($request->status);
|
||||||
if ($dbStatus === 'hired') {
|
if ($dbStatus === 'hired') {
|
||||||
$dbStatus = 'hire_requested';
|
$dbStatus = 'hire_requested';
|
||||||
|
} elseif ($dbStatus === 'review' || $dbStatus === 'reviewing') {
|
||||||
|
$dbStatus = 'applied';
|
||||||
}
|
}
|
||||||
|
|
||||||
// Contact Limit check: if moving to 'shortlisted', 'contacted', 'interview_scheduled', 'selected', 'hired', 'hire_requested'
|
// Contact Limit check: if moving to 'shortlisted', 'contacted', 'interview_scheduled', 'selected', 'hired', 'hire_requested'
|
||||||
@ -715,8 +811,16 @@ public function employerUpdateApplicationStatus(Request $request, $id)
|
|||||||
'notes' => $notes,
|
'notes' => $notes,
|
||||||
];
|
];
|
||||||
|
|
||||||
|
$empStatus = strtolower($request->status);
|
||||||
|
if ($empStatus === 'applied' || $empStatus === 'reviewing' || $empStatus === 'review') {
|
||||||
|
$empStatus = 'review';
|
||||||
|
} elseif ($empStatus === 'hire_requested') {
|
||||||
|
$empStatus = 'hired';
|
||||||
|
}
|
||||||
|
|
||||||
$updateData = [
|
$updateData = [
|
||||||
'status' => $dbStatus,
|
'status' => $dbStatus,
|
||||||
|
'employer_status' => $empStatus,
|
||||||
'status_history' => $history,
|
'status_history' => $history,
|
||||||
];
|
];
|
||||||
|
|
||||||
@ -785,6 +889,7 @@ public function appliedJobDetail(Request $request, $id)
|
|||||||
'application' => [
|
'application' => [
|
||||||
'id' => $application->id,
|
'id' => $application->id,
|
||||||
'status' => $application->status,
|
'status' => $application->status,
|
||||||
|
'employer_status' => $application->employer_status,
|
||||||
'status_history' => $application->status_history ?: [],
|
'status_history' => $application->status_history ?: [],
|
||||||
'applied_at' => $application->created_at->toIso8601String(),
|
'applied_at' => $application->created_at->toIso8601String(),
|
||||||
'job' => $job ? [
|
'job' => $job ? [
|
||||||
|
|||||||
@ -54,12 +54,12 @@ public function index(Request $request)
|
|||||||
if (!$w) return null;
|
if (!$w) return null;
|
||||||
|
|
||||||
// Map DB status to UI friendly status
|
// Map DB status to UI friendly status
|
||||||
$status = 'Reviewing';
|
$status = 'Review';
|
||||||
$dbStatusLower = strtolower($app->status ?? '');
|
$dbStatusLower = strtolower($app->status ?? '');
|
||||||
if ($dbStatusLower === 'hired') $status = 'Hired';
|
if ($dbStatusLower === 'hired') $status = 'Hired';
|
||||||
elseif ($dbStatusLower === 'rejected') $status = 'Rejected';
|
elseif ($dbStatusLower === 'rejected') $status = 'Rejected';
|
||||||
elseif ($dbStatusLower === 'shortlisted' || $dbStatusLower === 'offer_sent' || $dbStatusLower === 'offer sent') $status = 'Offer Sent';
|
elseif ($dbStatusLower === 'shortlisted' || $dbStatusLower === 'offer_sent' || $dbStatusLower === 'offer sent') $status = 'Shortlisted';
|
||||||
elseif ($dbStatusLower === 'applied') $status = 'Reviewing';
|
elseif ($dbStatusLower === 'applied' || $dbStatusLower === 'review' || $dbStatusLower === 'reviewing') $status = 'Review';
|
||||||
else $status = ucfirst($app->status);
|
else $status = ucfirst($app->status);
|
||||||
|
|
||||||
return [
|
return [
|
||||||
@ -316,7 +316,7 @@ private function checkJobAccess($user)
|
|||||||
public function updateStatus(Request $request, $id)
|
public function updateStatus(Request $request, $id)
|
||||||
{
|
{
|
||||||
$request->validate([
|
$request->validate([
|
||||||
'status' => 'required|string|in:Applied,Reviewing,Shortlisted,Contacted,Interview Scheduled,Selected,Rejected,Hired,Offer Sent,applied,shortlisted,contacted,interview scheduled,interview_scheduled,selected,rejected,hired,offer sent,offer_sent,hire_requested,hire_requested',
|
'status' => 'required|string|in:Applied,Review,review,Reviewing,reviewing,Shortlisted,Contacted,Interview Scheduled,Selected,Rejected,Hired,Offer Sent,applied,shortlisted,contacted,interview scheduled,interview_scheduled,selected,rejected,hired,offer sent,offer_sent,hire_requested',
|
||||||
'notes' => 'nullable|string',
|
'notes' => 'nullable|string',
|
||||||
]);
|
]);
|
||||||
|
|
||||||
@ -381,7 +381,7 @@ public function updateStatus(Request $request, $id)
|
|||||||
$dbStatus = 'interview_scheduled';
|
$dbStatus = 'interview_scheduled';
|
||||||
} elseif ($statusLower === 'selected') {
|
} elseif ($statusLower === 'selected') {
|
||||||
$dbStatus = 'selected';
|
$dbStatus = 'selected';
|
||||||
} elseif ($statusLower === 'reviewing' || $statusLower === 'applied') {
|
} elseif ($statusLower === 'reviewing' || $statusLower === 'applied' || $statusLower === 'review') {
|
||||||
$dbStatus = 'applied';
|
$dbStatus = 'applied';
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -408,8 +408,16 @@ public function updateStatus(Request $request, $id)
|
|||||||
'notes' => $request->input('notes'),
|
'notes' => $request->input('notes'),
|
||||||
];
|
];
|
||||||
|
|
||||||
|
$empStatus = strtolower($request->status);
|
||||||
|
if ($empStatus === 'applied' || $empStatus === 'reviewing' || $empStatus === 'review') {
|
||||||
|
$empStatus = 'review';
|
||||||
|
} elseif ($empStatus === 'hire_requested') {
|
||||||
|
$empStatus = 'hired';
|
||||||
|
}
|
||||||
|
|
||||||
$updateData = [
|
$updateData = [
|
||||||
'status' => $dbStatus,
|
'status' => $dbStatus,
|
||||||
|
'employer_status' => $empStatus,
|
||||||
'status_history' => $history,
|
'status_history' => $history,
|
||||||
];
|
];
|
||||||
|
|
||||||
|
|||||||
@ -197,10 +197,59 @@ public function show($id)
|
|||||||
];
|
];
|
||||||
})->toArray();
|
})->toArray();
|
||||||
|
|
||||||
|
$application = \App\Models\JobApplication::where('worker_id', $activeConv->worker_id)
|
||||||
|
->whereHas('jobPost', function($q) use ($user) {
|
||||||
|
$q->where('employer_id', $user->id);
|
||||||
|
})
|
||||||
|
->latest('id')
|
||||||
|
->first();
|
||||||
|
|
||||||
|
if (!$application) {
|
||||||
|
$jobPost = \App\Models\JobPost::where('employer_id', $user->id)->latest('id')->first();
|
||||||
|
if (!$jobPost) {
|
||||||
|
$jobPost = \App\Models\JobPost::create([
|
||||||
|
'employer_id' => $user->id,
|
||||||
|
'title' => 'General Helper',
|
||||||
|
'location' => 'Dubai',
|
||||||
|
'salary' => 2000,
|
||||||
|
'job_type' => 'full-time',
|
||||||
|
'status' => 'open',
|
||||||
|
'start_date' => now()->format('Y-m-d'),
|
||||||
|
'description' => 'General domestic helper position.',
|
||||||
|
'requirements' => 'General helper requirements.',
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
$application = \App\Models\JobApplication::create([
|
||||||
|
'job_id' => $jobPost->id,
|
||||||
|
'worker_id' => $activeConv->worker_id,
|
||||||
|
'status' => 'applied',
|
||||||
|
'employer_status' => 'review',
|
||||||
|
'status_history' => [
|
||||||
|
[
|
||||||
|
'status' => 'applied',
|
||||||
|
'timestamp' => now()->toIso8601String(),
|
||||||
|
'notes' => 'Application created via chat conversation.',
|
||||||
|
]
|
||||||
|
],
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
$applicationData = null;
|
||||||
|
if ($application) {
|
||||||
|
$applicationData = [
|
||||||
|
'application_id' => $application->id,
|
||||||
|
'status' => $application->status,
|
||||||
|
'notes' => $application->notes,
|
||||||
|
'status_history' => $application->status_history ?: [],
|
||||||
|
'name' => $activeConv->worker->name ?? 'Candidate',
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
return Inertia::render('Employer/Messages/Show', [
|
return Inertia::render('Employer/Messages/Show', [
|
||||||
'conversations' => $conversations,
|
'conversations' => $conversations,
|
||||||
'conversation' => $conversationData,
|
'conversation' => $conversationData,
|
||||||
'initialMessages' => $initialMessages,
|
'initialMessages' => $initialMessages,
|
||||||
|
'application' => $applicationData,
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -13,6 +13,7 @@ class JobApplication extends Model
|
|||||||
'job_id',
|
'job_id',
|
||||||
'worker_id',
|
'worker_id',
|
||||||
'status',
|
'status',
|
||||||
|
'employer_status',
|
||||||
'notes',
|
'notes',
|
||||||
'status_history',
|
'status_history',
|
||||||
'joining_confirmed_at',
|
'joining_confirmed_at',
|
||||||
|
|||||||
@ -0,0 +1,28 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
use Illuminate\Database\Migrations\Migration;
|
||||||
|
use Illuminate\Database\Schema\Blueprint;
|
||||||
|
use Illuminate\Support\Facades\Schema;
|
||||||
|
|
||||||
|
return new class extends Migration
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Run the migrations.
|
||||||
|
*/
|
||||||
|
public function up(): void
|
||||||
|
{
|
||||||
|
Schema::table('job_applications', function (Blueprint $table) {
|
||||||
|
$table->string('employer_status')->nullable()->after('status');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reverse the migrations.
|
||||||
|
*/
|
||||||
|
public function down(): void
|
||||||
|
{
|
||||||
|
Schema::table('job_applications', function (Blueprint $table) {
|
||||||
|
$table->dropColumn('employer_status');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
@ -4138,6 +4138,56 @@
|
|||||||
],
|
],
|
||||||
"summary": "List Available Jobs",
|
"summary": "List Available Jobs",
|
||||||
"description": "Returns a list of all active job postings available for workers to discover and apply.",
|
"description": "Returns a list of all active job postings available for workers to discover and apply.",
|
||||||
|
"parameters": [
|
||||||
|
{
|
||||||
|
"name": "job_type",
|
||||||
|
"in": "query",
|
||||||
|
"schema": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"description": "Direct filter by exact job type (e.g., 'Full Time', 'Part Time', 'Contract')."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "skills",
|
||||||
|
"in": "query",
|
||||||
|
"schema": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"description": "Comma-separated list of skill names or IDs to filter by (e.g. 'Cooking,Cleaning')."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "salary_min",
|
||||||
|
"in": "query",
|
||||||
|
"schema": {
|
||||||
|
"type": "number"
|
||||||
|
},
|
||||||
|
"description": "Minimum salary filter."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "salary_max",
|
||||||
|
"in": "query",
|
||||||
|
"schema": {
|
||||||
|
"type": "number"
|
||||||
|
},
|
||||||
|
"description": "Maximum salary filter."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "search",
|
||||||
|
"in": "query",
|
||||||
|
"schema": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"description": "General search query for title, description, or location."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "sortBy",
|
||||||
|
"in": "query",
|
||||||
|
"schema": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"description": "Sorting order (e.g. 'LATEST FIRST', 'OLDEST FIRST', 'PRICE: LOW TO HIGH', 'PRICE: HIGH TO LOW')."
|
||||||
|
}
|
||||||
|
],
|
||||||
"security": [
|
"security": [
|
||||||
{
|
{
|
||||||
"bearerAuth": []
|
"bearerAuth": []
|
||||||
|
|||||||
@ -37,7 +37,18 @@ export default function Applicants({ job, applicants: initialApplicants, isShort
|
|||||||
|
|
||||||
const openStatusModal = (app, statusPreset = '') => {
|
const openStatusModal = (app, statusPreset = '') => {
|
||||||
setSelectedApp(app);
|
setSelectedApp(app);
|
||||||
setNewStatus(statusPreset || app.status || 'Applied');
|
let resolvedStatus = statusPreset || app.status || 'Applied';
|
||||||
|
const lowerStatus = resolvedStatus.toLowerCase();
|
||||||
|
if (lowerStatus === 'hire_requested' || lowerStatus === 'hired') {
|
||||||
|
resolvedStatus = 'Hired';
|
||||||
|
} else if (lowerStatus === 'rejected') {
|
||||||
|
resolvedStatus = 'Rejected';
|
||||||
|
} else if (lowerStatus === 'shortlisted') {
|
||||||
|
resolvedStatus = 'Shortlisted';
|
||||||
|
} else {
|
||||||
|
resolvedStatus = 'Applied';
|
||||||
|
}
|
||||||
|
setNewStatus(resolvedStatus);
|
||||||
setNotes(app.notes || '');
|
setNotes(app.notes || '');
|
||||||
setShowModal(true);
|
setShowModal(true);
|
||||||
};
|
};
|
||||||
@ -197,7 +208,8 @@ export default function Applicants({ job, applicants: initialApplicants, isShort
|
|||||||
applicant.status?.toLowerCase() === 'rejected' ? 'bg-rose-100 text-rose-700' :
|
applicant.status?.toLowerCase() === 'rejected' ? 'bg-rose-100 text-rose-700' :
|
||||||
'bg-blue-50 text-[#185FA5]'
|
'bg-blue-50 text-[#185FA5]'
|
||||||
}`} onClick={() => openStatusModal(applicant)}>
|
}`} onClick={() => openStatusModal(applicant)}>
|
||||||
{applicant.status?.toLowerCase() === 'hire_requested' ? 'Pending Confirmation' : applicant.status}
|
{applicant.status?.toLowerCase() === 'hire_requested' ? 'Pending Confirmation' :
|
||||||
|
applicant.status?.toLowerCase() === 'applied' ? 'Review' : applicant.status}
|
||||||
</span>
|
</span>
|
||||||
<div className="flex items-center space-x-1 text-[10px] font-bold text-slate-400">
|
<div className="flex items-center space-x-1 text-[10px] font-bold text-slate-400">
|
||||||
<Link href={`/employer/workers/${applicant.id}`} className="hover:text-[#185FA5] flex items-center underline underline-offset-2">
|
<Link href={`/employer/workers/${applicant.id}`} className="hover:text-[#185FA5] flex items-center underline underline-offset-2">
|
||||||
@ -275,13 +287,10 @@ export default function Applicants({ job, applicants: initialApplicants, isShort
|
|||||||
onChange={(e) => setNewStatus(e.target.value)}
|
onChange={(e) => setNewStatus(e.target.value)}
|
||||||
className="w-full px-4 py-3 bg-slate-50 border border-slate-100 rounded-xl text-xs font-bold text-slate-900 focus:ring-4 focus:ring-blue-500/10 focus:border-[#185FA5] outline-none transition-all"
|
className="w-full px-4 py-3 bg-slate-50 border border-slate-100 rounded-xl text-xs font-bold text-slate-900 focus:ring-4 focus:ring-blue-500/10 focus:border-[#185FA5] outline-none transition-all"
|
||||||
>
|
>
|
||||||
<option value="Applied">Applied (Reviewing)</option>
|
<option value="Applied">Review</option>
|
||||||
<option value="Shortlisted">Shortlisted (Save Candidate)</option>
|
<option value="Shortlisted">Shortlisted</option>
|
||||||
<option value="Contacted">Contacted</option>
|
|
||||||
<option value="Interview Scheduled">Interview Scheduled</option>
|
|
||||||
<option value="Selected">Selected</option>
|
|
||||||
<option value="Rejected">Rejected</option>
|
<option value="Rejected">Rejected</option>
|
||||||
<option value="Hired">Hired (Request Confirmation)</option>
|
<option value="Hired">Hired</option>
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@ -32,13 +32,16 @@ import {
|
|||||||
} from 'lucide-react';
|
} from 'lucide-react';
|
||||||
import { toast } from 'sonner';
|
import { toast } from 'sonner';
|
||||||
|
|
||||||
export default function Show({ conversation, initialMessages, conversations = [] }) {
|
export default function Show({ conversation, initialMessages, conversations = [], application = null }) {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const [messages, setMessages] = useState(initialMessages || []);
|
const [messages, setMessages] = useState(initialMessages || []);
|
||||||
const [input, setInput] = useState('');
|
const [input, setInput] = useState('');
|
||||||
const [isTyping, setIsTyping] = useState(false);
|
const [isTyping, setIsTyping] = useState(false);
|
||||||
const [searchTerm, setSearchTerm] = useState('');
|
const [searchTerm, setSearchTerm] = useState('');
|
||||||
const [showHireConfirmModal, setShowHireConfirmModal] = useState(false);
|
const [selectedApp, setSelectedApp] = useState(null);
|
||||||
|
const [newStatus, setNewStatus] = useState('');
|
||||||
|
const [notes, setNotes] = useState('');
|
||||||
|
const [showModal, setShowModal] = useState(false);
|
||||||
const [showAttachmentModal, setShowAttachmentModal] = useState(false);
|
const [showAttachmentModal, setShowAttachmentModal] = useState(false);
|
||||||
const [uploadedFiles, setUploadedFiles] = useState([]);
|
const [uploadedFiles, setUploadedFiles] = useState([]);
|
||||||
const [isRecording, setIsRecording] = useState(false);
|
const [isRecording, setIsRecording] = useState(false);
|
||||||
@ -189,14 +192,39 @@ export default function Show({ conversation, initialMessages, conversations = []
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleMarkHired = () => {
|
const openStatusModal = (app, statusPreset = '') => {
|
||||||
router.post(`/employer/workers/${conversation.worker_id}/mark-hired`, {}, {
|
setSelectedApp(app);
|
||||||
|
let resolvedStatus = statusPreset || app.status || 'Applied';
|
||||||
|
const lowerStatus = resolvedStatus.toLowerCase();
|
||||||
|
if (lowerStatus === 'hire_requested' || lowerStatus === 'hired') {
|
||||||
|
resolvedStatus = 'Hired';
|
||||||
|
} else if (lowerStatus === 'rejected') {
|
||||||
|
resolvedStatus = 'Rejected';
|
||||||
|
} else if (lowerStatus === 'shortlisted') {
|
||||||
|
resolvedStatus = 'Shortlisted';
|
||||||
|
} else {
|
||||||
|
resolvedStatus = 'Applied';
|
||||||
|
}
|
||||||
|
setNewStatus(resolvedStatus);
|
||||||
|
setNotes(app.notes || '');
|
||||||
|
setShowModal(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSaveStatus = () => {
|
||||||
|
if (!selectedApp) return;
|
||||||
|
|
||||||
|
router.post(`/employer/candidates/${selectedApp.application_id}/status`, {
|
||||||
|
status: newStatus,
|
||||||
|
notes: notes
|
||||||
|
}, {
|
||||||
preserveScroll: true,
|
preserveScroll: true,
|
||||||
onSuccess: () => {
|
onSuccess: () => {
|
||||||
toast.success(t('hire_request_sent', 'Hiring request sent successfully!'), {
|
toast.success(t('status_updated_success', 'Applicant status updated successfully.'));
|
||||||
description: t('hire_request_sent_desc', 'Awaiting worker confirmation of the hiring offer.')
|
setShowModal(false);
|
||||||
});
|
setSelectedApp(null);
|
||||||
router.reload();
|
},
|
||||||
|
onError: (errors) => {
|
||||||
|
toast.error(t('status_updated_error', 'Failed to update status: ' + (errors.error || errors.status || 'Error occurred')));
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
@ -315,25 +343,15 @@ export default function Show({ conversation, initialMessages, conversations = []
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex items-center space-x-2">
|
<div className="flex items-center space-x-2">
|
||||||
{conversation.worker_status === 'pending_confirmation' ? (
|
{application ? (
|
||||||
<span className="px-3.5 py-2 bg-amber-50 text-amber-700 rounded-xl text-xs font-black uppercase tracking-wider border border-amber-200 flex items-center space-x-1.5">
|
|
||||||
<Clock className="w-4 h-4 text-amber-600 animate-pulse" />
|
|
||||||
<span>{t('pending_confirmation', 'Pending Confirmation')}</span>
|
|
||||||
</span>
|
|
||||||
) : conversation.worker_status !== 'hired' && conversation.worker_status !== 'hidden' ? (
|
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => setShowHireConfirmModal(true)}
|
onClick={() => openStatusModal(application)}
|
||||||
className="px-3.5 py-2 bg-emerald-600 text-white hover:bg-emerald-700 rounded-xl transition-all shadow-md flex items-center space-x-1.5 text-xs font-black uppercase tracking-wider border border-emerald-700 cursor-pointer"
|
className="px-3.5 py-2 bg-emerald-600 text-white hover:bg-emerald-700 rounded-xl transition-all shadow-md flex items-center space-x-1.5 text-xs font-black uppercase tracking-wider border border-emerald-700 cursor-pointer"
|
||||||
>
|
>
|
||||||
<CheckCircle2 className="w-4 h-4 text-white" />
|
<CheckCircle2 className="w-4 h-4 text-white" />
|
||||||
<span>{t('mark_hired_action', 'Mark Hired')}</span>
|
<span>{t('update_status', 'Update Status')}</span>
|
||||||
</button>
|
</button>
|
||||||
) : conversation.worker_status === 'hired' ? (
|
|
||||||
<span className="px-3.5 py-2 bg-slate-100 text-slate-500 rounded-xl text-xs font-black uppercase tracking-wider border border-slate-200 flex items-center space-x-1.5">
|
|
||||||
<CheckCircle2 className="w-4 h-4 text-emerald-600" />
|
|
||||||
<span>{t('hired', 'Hired')}</span>
|
|
||||||
</span>
|
|
||||||
) : null}
|
) : null}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -537,38 +555,90 @@ export default function Show({ conversation, initialMessages, conversations = []
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Hired Confirmation Modal */}
|
{/* Status Update Modal */}
|
||||||
{showHireConfirmModal && (
|
{showModal && (
|
||||||
<div className="fixed inset-0 bg-slate-900/60 backdrop-blur-xs flex items-center justify-center p-4 z-50 animate-fade-in">
|
<div className="fixed inset-0 bg-slate-900/60 backdrop-blur-xs flex items-center justify-center p-4 z-50 animate-fade-in">
|
||||||
<div className="bg-white rounded-3xl w-full max-w-md p-6 relative animate-zoom-in text-center space-y-4 border border-slate-200 shadow-2xl">
|
<div className="bg-white w-full max-w-lg rounded-[28px] shadow-2xl border border-slate-200 overflow-hidden flex flex-col animate-in fade-in zoom-in-95 duration-200 text-left">
|
||||||
<div className="mx-auto w-16 h-16 rounded-full bg-blue-50 border border-blue-100 flex items-center justify-center text-blue-600">
|
{/* Modal Header */}
|
||||||
<CheckCircle2 className="w-8 h-8" />
|
<div className="p-6 border-b border-slate-100 flex items-center justify-between">
|
||||||
|
<div>
|
||||||
|
<h3 className="font-black text-slate-900 text-base">Update Application Status</h3>
|
||||||
|
<p className="text-[10px] font-bold text-[#185FA5] uppercase tracking-widest mt-0.5">{selectedApp?.name}</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="space-y-2">
|
|
||||||
<h4 className="font-extrabold text-lg text-slate-900">{t('confirm_hire_title', 'Confirm Hiring')}</h4>
|
|
||||||
<p className="text-xs text-slate-600 leading-normal px-2">
|
|
||||||
{t('confirm_hire_desc', 'Verify this worker with your needs and make sure to hire this worker. If hired, you can view this worker in the Hired Workers page.')}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="flex space-x-3 pt-2 text-xs font-bold">
|
|
||||||
<button
|
<button
|
||||||
type="button"
|
onClick={() => { setShowModal(false); setSelectedApp(null); }}
|
||||||
onClick={() => setShowHireConfirmModal(false)}
|
className="p-2 hover:bg-slate-50 text-slate-400 hover:text-slate-950 rounded-full transition-colors cursor-pointer"
|
||||||
className="flex-1 py-3 border border-slate-200 hover:bg-slate-50 rounded-xl transition-colors"
|
|
||||||
>
|
>
|
||||||
{t('cancel', 'Cancel')}
|
<X className="w-5 h-5" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Modal Body */}
|
||||||
|
<div className="p-6 space-y-5">
|
||||||
|
{selectedApp?.status?.toLowerCase() === 'hire_requested' && (
|
||||||
|
<div className="p-3 bg-amber-50 rounded-xl border border-amber-100 text-[10px] text-amber-700 font-bold">
|
||||||
|
A hiring offer is pending. Waiting for worker confirmation.
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{/* Status Selector */}
|
||||||
|
<div className="space-y-2">
|
||||||
|
<label className="text-[10px] font-black text-slate-400 uppercase tracking-widest">Select Stage</label>
|
||||||
|
<select
|
||||||
|
value={newStatus}
|
||||||
|
onChange={(e) => setNewStatus(e.target.value)}
|
||||||
|
className="w-full px-4 py-3 bg-slate-50 border border-slate-100 rounded-xl text-xs font-bold text-slate-900 focus:ring-4 focus:ring-blue-500/10 focus:border-[#185FA5] outline-none transition-all cursor-pointer"
|
||||||
|
>
|
||||||
|
<option value="Applied">Review</option>
|
||||||
|
<option value="Shortlisted">Shortlisted</option>
|
||||||
|
<option value="Rejected">Rejected</option>
|
||||||
|
<option value="Hired">Hired</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Notes Textarea */}
|
||||||
|
<div className="space-y-2">
|
||||||
|
<label className="text-[10px] font-black text-slate-400 uppercase tracking-widest">Internal Employer Notes</label>
|
||||||
|
<textarea
|
||||||
|
value={notes}
|
||||||
|
onChange={(e) => setNotes(e.target.value)}
|
||||||
|
placeholder="Add any internal feedback, interview times, or notes about this candidate..."
|
||||||
|
rows={4}
|
||||||
|
className="w-full px-4 py-3 bg-slate-50 border border-slate-100 rounded-xl text-xs font-medium text-slate-900 placeholder-slate-400 focus:ring-4 focus:ring-blue-500/10 focus:border-[#185FA5] outline-none transition-all resize-none"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Status History Audit Trail */}
|
||||||
|
{selectedApp?.status_history && selectedApp.status_history.length > 0 && (
|
||||||
|
<div className="space-y-2 pt-2">
|
||||||
|
<label className="text-[10px] font-black text-slate-400 uppercase tracking-widest">Status Audit Log</label>
|
||||||
|
<div className="max-h-24 overflow-y-auto space-y-2 pr-1 border border-slate-100 rounded-xl p-2 bg-slate-50/40">
|
||||||
|
{selectedApp.status_history.map((hist, index) => (
|
||||||
|
<div key={index} className="text-[9px] text-slate-500 flex justify-between items-start">
|
||||||
|
<div>
|
||||||
|
<span className="font-bold uppercase text-[#185FA5]">{hist.status}</span>
|
||||||
|
{hist.notes && <span className="ml-1 text-slate-400 italic">({hist.notes})</span>}
|
||||||
|
</div>
|
||||||
|
<span className="text-[8px] font-mono text-slate-300">{new Date(hist.timestamp).toLocaleDateString()}</span>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Modal Footer */}
|
||||||
|
<div className="p-6 bg-slate-50 border-t border-slate-100 flex items-center justify-end space-x-3">
|
||||||
|
<button
|
||||||
|
onClick={() => { setShowModal(false); setSelectedApp(null); }}
|
||||||
|
className="px-5 py-2.5 bg-white border border-slate-200 hover:bg-slate-100 rounded-xl text-[10px] font-black uppercase tracking-widest text-slate-600 transition-colors cursor-pointer"
|
||||||
|
>
|
||||||
|
Cancel
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
type="button"
|
onClick={handleSaveStatus}
|
||||||
onClick={() => {
|
className="px-5 py-2.5 bg-[#185FA5] hover:bg-[#144f8a] text-white rounded-xl text-[10px] font-black uppercase tracking-widest transition-all shadow-md shadow-blue-500/10 cursor-pointer"
|
||||||
setShowHireConfirmModal(false);
|
|
||||||
handleMarkHired();
|
|
||||||
}}
|
|
||||||
className="flex-1 bg-emerald-600 hover:bg-emerald-700 text-white py-3 rounded-xl shadow-sm transition-colors"
|
|
||||||
>
|
>
|
||||||
{t('confirm_hire_action', 'Confirm Hire')}
|
Save Status
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
120
tests/Feature/EmployerMessageWebTest.php
Normal file
120
tests/Feature/EmployerMessageWebTest.php
Normal file
@ -0,0 +1,120 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Tests\Feature;
|
||||||
|
|
||||||
|
use App\Models\Conversation;
|
||||||
|
use App\Models\Message;
|
||||||
|
use App\Models\User;
|
||||||
|
use App\Models\EmployerProfile;
|
||||||
|
use App\Models\Worker;
|
||||||
|
use App\Models\JobPost;
|
||||||
|
use App\Models\JobApplication;
|
||||||
|
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||||
|
use Tests\TestCase;
|
||||||
|
use Inertia\Testing\AssertableInertia as Assert;
|
||||||
|
|
||||||
|
class EmployerMessageWebTest extends TestCase
|
||||||
|
{
|
||||||
|
use RefreshDatabase;
|
||||||
|
|
||||||
|
protected $worker;
|
||||||
|
protected $employer;
|
||||||
|
protected $jobPost;
|
||||||
|
|
||||||
|
protected function setUp(): void
|
||||||
|
{
|
||||||
|
parent::setUp();
|
||||||
|
|
||||||
|
// Create a test worker
|
||||||
|
$this->worker = Worker::create([
|
||||||
|
'name' => 'Jane Smith',
|
||||||
|
'email' => 'jane@example.com',
|
||||||
|
'phone' => '+971500000001',
|
||||||
|
'password' => bcrypt('password'),
|
||||||
|
'status' => 'Available',
|
||||||
|
'nationality' => 'Ethiopian',
|
||||||
|
'age' => 28,
|
||||||
|
'salary' => 1500.00,
|
||||||
|
'availability' => 'Immediate',
|
||||||
|
'experience' => '2 Years',
|
||||||
|
'religion' => 'Christian',
|
||||||
|
'bio' => 'Experienced housemaid seeking employment.',
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Create an employer user
|
||||||
|
$this->employer = User::create([
|
||||||
|
'name' => 'John Employer',
|
||||||
|
'email' => 'employer_test@example.com',
|
||||||
|
'password' => bcrypt('password'),
|
||||||
|
'role' => 'employer',
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Create employer profile
|
||||||
|
EmployerProfile::create([
|
||||||
|
'user_id' => $this->employer->id,
|
||||||
|
'company_name' => 'John Ltd',
|
||||||
|
'phone' => '+971500000002',
|
||||||
|
'country' => 'United Arab Emirates',
|
||||||
|
'verification_status' => 'approved',
|
||||||
|
'language' => 'English',
|
||||||
|
'notifications' => true,
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Create a Job Post
|
||||||
|
$this->jobPost = JobPost::create([
|
||||||
|
'employer_id' => $this->employer->id,
|
||||||
|
'title' => 'Housekeeper',
|
||||||
|
'location' => 'Dubai',
|
||||||
|
'salary' => 1800,
|
||||||
|
'job_type' => 'full-time',
|
||||||
|
'status' => 'open',
|
||||||
|
'start_date' => '2026-08-01',
|
||||||
|
'description' => 'Test Job Description',
|
||||||
|
'requirements' => 'Test Requirements',
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Test show conversation auto-creates JobApplication and passes it as a prop.
|
||||||
|
*/
|
||||||
|
public function test_show_conversation_auto_creates_application_and_passes_prop()
|
||||||
|
{
|
||||||
|
// Authenticate session
|
||||||
|
session(['user' => (object)[
|
||||||
|
'id' => $this->employer->id,
|
||||||
|
'name' => $this->employer->name,
|
||||||
|
'email' => $this->employer->email,
|
||||||
|
'role' => 'employer',
|
||||||
|
'subscription_status' => 'active',
|
||||||
|
]]);
|
||||||
|
|
||||||
|
$conversation = Conversation::create([
|
||||||
|
'employer_id' => $this->employer->id,
|
||||||
|
'worker_id' => $this->worker->id,
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Ensure no application exists initially
|
||||||
|
$this->assertDatabaseMissing('job_applications', [
|
||||||
|
'worker_id' => $this->worker->id,
|
||||||
|
]);
|
||||||
|
|
||||||
|
$response = $this->actingAs($this->employer)
|
||||||
|
->get(route('employer.messages.show', ['id' => $conversation->id]));
|
||||||
|
|
||||||
|
$response->assertStatus(200);
|
||||||
|
|
||||||
|
// Verify JobApplication was auto-created in database
|
||||||
|
$this->assertDatabaseHas('job_applications', [
|
||||||
|
'worker_id' => $this->worker->id,
|
||||||
|
'status' => 'applied',
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Verify Inertia response has the 'application' prop
|
||||||
|
$response->assertInertia(fn (Assert $page) => $page
|
||||||
|
->component('Employer/Messages/Show')
|
||||||
|
->has('application')
|
||||||
|
->where('application.status', 'applied')
|
||||||
|
->has('application.status_history')
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -166,6 +166,7 @@ public function test_worker_job_apis()
|
|||||||
$response->assertJsonCount(1, 'data.jobs');
|
$response->assertJsonCount(1, 'data.jobs');
|
||||||
$response->assertJsonPath('data.jobs.0.title', 'Available Plumber Job');
|
$response->assertJsonPath('data.jobs.0.title', 'Available Plumber Job');
|
||||||
$response->assertJsonPath('data.jobs.0.posted_by', 'Employer User');
|
$response->assertJsonPath('data.jobs.0.posted_by', 'Employer User');
|
||||||
|
$response->assertJsonPath('data.jobs.0.status', 'new');
|
||||||
|
|
||||||
// 2. Retrieve job details
|
// 2. Retrieve job details
|
||||||
$response = $this->getJson("/api/workers/jobs/{$job->id}", [
|
$response = $this->getJson("/api/workers/jobs/{$job->id}", [
|
||||||
@ -175,6 +176,7 @@ public function test_worker_job_apis()
|
|||||||
$response->assertJsonPath('success', true);
|
$response->assertJsonPath('success', true);
|
||||||
$response->assertJsonPath('data.job.title', 'Available Plumber Job');
|
$response->assertJsonPath('data.job.title', 'Available Plumber Job');
|
||||||
$response->assertJsonPath('data.job.posted_by', 'Employer User');
|
$response->assertJsonPath('data.job.posted_by', 'Employer User');
|
||||||
|
$response->assertJsonPath('data.job.status', 'new');
|
||||||
|
|
||||||
// 3. Apply for job
|
// 3. Apply for job
|
||||||
$response = $this->postJson("/api/workers/jobs/{$job->id}/apply", [], [
|
$response = $this->postJson("/api/workers/jobs/{$job->id}/apply", [], [
|
||||||
@ -188,6 +190,13 @@ public function test_worker_job_apis()
|
|||||||
'status' => 'applied',
|
'status' => 'applied',
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
// Verify status becomes 'applied' in list available jobs
|
||||||
|
$response = $this->getJson('/api/workers/jobs', [
|
||||||
|
'Authorization' => 'Bearer worker_api_token'
|
||||||
|
]);
|
||||||
|
$response->assertStatus(200);
|
||||||
|
$response->assertJsonPath('data.jobs.0.status', 'applied');
|
||||||
|
|
||||||
// 4. Prevent duplicate job applications
|
// 4. Prevent duplicate job applications
|
||||||
$response = $this->postJson("/api/workers/jobs/{$job->id}/apply", [], [
|
$response = $this->postJson("/api/workers/jobs/{$job->id}/apply", [], [
|
||||||
'Authorization' => 'Bearer worker_api_token'
|
'Authorization' => 'Bearer worker_api_token'
|
||||||
@ -728,6 +737,90 @@ public function test_worker_can_view_job_matching_only_skills()
|
|||||||
'title' => 'Electrician Needed'
|
'title' => 'Electrician Needed'
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function test_worker_can_filter_and_sort_jobs()
|
||||||
|
{
|
||||||
|
// 1. Create jobs with different contractTypes and salaries
|
||||||
|
$job1 = JobPost::create([
|
||||||
|
'employer_id' => $this->employer->id,
|
||||||
|
'title' => 'Contract Plumber Job',
|
||||||
|
'main_profession' => 'Plumber',
|
||||||
|
'location' => 'Dubai Marina',
|
||||||
|
'salary' => 1000,
|
||||||
|
'workers_needed' => 1,
|
||||||
|
'job_type' => 'Contract',
|
||||||
|
'start_date' => now()->addDays(2),
|
||||||
|
'description' => 'Fix pipes description',
|
||||||
|
'status' => 'active',
|
||||||
|
]);
|
||||||
|
$skill = \App\Models\Skill::firstOrCreate(['name' => 'Piping']);
|
||||||
|
$job1->skills()->sync([$skill->id]);
|
||||||
|
|
||||||
|
$job2 = JobPost::create([
|
||||||
|
'employer_id' => $this->employer->id,
|
||||||
|
'title' => 'Full Time Plumber Job',
|
||||||
|
'main_profession' => 'Plumber',
|
||||||
|
'location' => 'Dubai Marina',
|
||||||
|
'salary' => 5000,
|
||||||
|
'workers_needed' => 1,
|
||||||
|
'job_type' => 'Full Time',
|
||||||
|
'start_date' => now()->addDays(2),
|
||||||
|
'description' => 'Manage plumbing description',
|
||||||
|
'status' => 'active',
|
||||||
|
]);
|
||||||
|
$job2->skills()->sync([$skill->id]);
|
||||||
|
|
||||||
|
// 2. Filter by job_type = Contract
|
||||||
|
$response = $this->getJson('/api/workers/jobs?job_type=Contract', [
|
||||||
|
'Authorization' => 'Bearer worker_api_token'
|
||||||
|
]);
|
||||||
|
$response->assertStatus(200);
|
||||||
|
$response->assertJsonFragment(['title' => 'Contract Plumber Job']);
|
||||||
|
$response->assertJsonMissingExact(['title' => 'Full Time Plumber Job']);
|
||||||
|
|
||||||
|
// 3. Search for "Manage"
|
||||||
|
$response = $this->getJson('/api/workers/jobs?search=Manage', [
|
||||||
|
'Authorization' => 'Bearer worker_api_token'
|
||||||
|
]);
|
||||||
|
$response->assertStatus(200);
|
||||||
|
$response->assertJsonFragment(['title' => 'Full Time Plumber Job']);
|
||||||
|
$response->assertJsonMissingExact(['title' => 'Contract Plumber Job']);
|
||||||
|
|
||||||
|
// 4. Sort by PRICE: HIGH TO LOW
|
||||||
|
$response = $this->getJson('/api/workers/jobs?sortBy=PRICE:%20HIGH%20TO%20LOW', [
|
||||||
|
'Authorization' => 'Bearer worker_api_token'
|
||||||
|
]);
|
||||||
|
$response->assertStatus(200);
|
||||||
|
// The first job in list should be the one with salary 5000 (Job 2)
|
||||||
|
$this->assertEquals('Full Time Plumber Job', $response->json('data.jobs.0.title'));
|
||||||
|
|
||||||
|
// 5. Filter by job_type directly
|
||||||
|
$response = $this->getJson('/api/workers/jobs?job_type=Full%20Time', [
|
||||||
|
'Authorization' => 'Bearer worker_api_token'
|
||||||
|
]);
|
||||||
|
$response->assertStatus(200);
|
||||||
|
$response->assertJsonFragment(['title' => 'Full Time Plumber Job']);
|
||||||
|
$response->assertJsonMissingExact(['title' => 'Contract Plumber Job']);
|
||||||
|
|
||||||
|
// 6. Filter by skills
|
||||||
|
$cookingSkill = \App\Models\Skill::firstOrCreate(['name' => 'Cooking']);
|
||||||
|
$job2->skills()->sync([$skill->id, $cookingSkill->id]);
|
||||||
|
|
||||||
|
$response = $this->getJson('/api/workers/jobs?skills=Cooking', [
|
||||||
|
'Authorization' => 'Bearer worker_api_token'
|
||||||
|
]);
|
||||||
|
$response->assertStatus(200);
|
||||||
|
$response->assertJsonFragment(['title' => 'Full Time Plumber Job']);
|
||||||
|
$response->assertJsonMissingExact(['title' => 'Contract Plumber Job']);
|
||||||
|
|
||||||
|
// 7. Filter by salary range
|
||||||
|
$response = $this->getJson('/api/workers/jobs?salary_min=2000&salary_max=6000', [
|
||||||
|
'Authorization' => 'Bearer worker_api_token'
|
||||||
|
]);
|
||||||
|
$response->assertStatus(200);
|
||||||
|
$response->assertJsonFragment(['title' => 'Full Time Plumber Job']);
|
||||||
|
$response->assertJsonMissingExact(['title' => 'Contract Plumber Job']);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user