Compare commits

..

2 Commits

Author SHA1 Message Date
38aae3e6a3 Merge pull request 'mohan' (#26) from mohan into master
Reviewed-on: #26
2026-07-09 10:16:33 +00:00
9077d40064 Merge pull request 'mohan' (#25) from mohan into master
Reviewed-on: #25
2026-07-04 16:10:26 +00:00
15 changed files with 390 additions and 494 deletions

View File

@ -772,14 +772,14 @@ public function hireCandidate(Request $request, $id = null)
], 403);
}
// Find existing offer or application if targeting by Worker ID directly
$offer = null;
$application = null;
// Direct Offer check
if (is_string($targetId) && str_starts_with($targetId, 'offer_')) {
$offerId = substr($targetId, 6);
$offer = JobOffer::where('id', $offerId)->where('employer_id', $employerId)->firstOrFail();
$offer->update(['status' => 'pending']);
$worker = $offer->worker;
$updatedOffer = $offer;
} else {
// Check if targetId is an application
$application = JobApplication::where('id', $targetId)
@ -788,7 +788,9 @@ public function hireCandidate(Request $request, $id = null)
})->first();
if ($application) {
$application->update(['status' => 'hire_requested']);
$worker = $application->worker;
$updatedApplication = $application;
} else {
// Try targeting by Worker ID directly
$worker = Worker::find($targetId);
@ -800,80 +802,37 @@ public function hireCandidate(Request $request, $id = null)
], 404);
}
// Find existing direct offer
// Find existing direct offer or create a new one with status 'pending'
$offer = JobOffer::where('employer_id', $employerId)
->where('worker_id', $worker->id)
->first();
if (!$offer) {
if ($offer) {
$offer->update(['status' => 'pending']);
$updatedOffer = $offer;
} else {
// Find existing application
$jobIds = JobPost::where('employer_id', $employerId)->pluck('id');
$application = JobApplication::whereIn('job_id', $jobIds)
->where('worker_id', $worker->id)
->first();
}
}
}
// Determine if this is first click ("Hire") or second click ("Confirm Hire")
$isWaitingForReview = false;
if ($offer && $offer->status === 'waiting_for_review') {
$isWaitingForReview = true;
} elseif ($application && $application->status === 'waiting_for_review') {
$isWaitingForReview = true;
}
if (!$isWaitingForReview) {
// Step 1: Transition to 'waiting_for_review'
if ($offer) {
$offer->update(['status' => 'waiting_for_review']);
} elseif ($application) {
$application->update(['status' => 'waiting_for_review']);
$history = $application->status_history ?: [];
$history[] = [
'status' => 'waiting_for_review',
'timestamp' => now()->toIso8601String(),
'notes' => 'Candidate status moved to waiting for review via API.',
];
$application->update(['status_history' => $history]);
if ($application) {
$application->update(['status' => 'hire_requested']);
$updatedApplication = $application;
} else {
// Create new offer in 'waiting_for_review' status
$offer = JobOffer::create([
// Create a new direct job offer in 'pending' status
$updatedOffer = JobOffer::create([
'employer_id' => $employerId,
'worker_id' => $worker->id,
'work_date' => now()->format('Y-m-d'),
'location' => 'Dubai',
'salary' => $worker->salary,
'notes' => 'Direct sponsoring matching initiated via API.',
'status' => 'waiting_for_review',
'status' => 'pending',
]);
}
return response()->json([
'success' => true,
'message' => 'Candidate status updated to Waiting for Review.',
'data' => [
'worker_id' => $worker->id,
'worker_status' => 'Waiting for Review',
'application' => $application,
'offer' => $offer,
]
], 200);
} else {
// Step 2: Confirm hire
if ($offer) {
$offer->update(['status' => 'pending']);
$updatedOffer = $offer;
} elseif ($application) {
$application->update(['status' => 'hire_requested']);
$history = $application->status_history ?: [];
$history[] = [
'status' => 'hire_requested',
'timestamp' => now()->toIso8601String(),
'notes' => 'Hire request initiated by employer.',
];
$application->update(['status_history' => $history]);
$updatedApplication = $application;
}
}
}
@ -1032,28 +991,6 @@ public function getWorkerDetail(Request $request, $id)
->where('worker_id', $w->id)
->first();
$pendingOfferOrApp = JobOffer::where('employer_id', $employer->id)
->where('worker_id', $w->id)
->where('status', 'pending')
->exists() ||
\App\Models\JobApplication::where('worker_id', $w->id)
->whereHas('jobPost', function($q) use ($employer) {
$q->where('employer_id', $employer->id);
})->where('status', 'hire_requested')
->exists();
$waitingForReviewOfferOrApp = JobOffer::where('employer_id', $employer->id)
->where('worker_id', $w->id)
->where('status', 'waiting_for_review')
->exists() ||
\App\Models\JobApplication::where('worker_id', $w->id)
->whereHas('jobPost', function($q) use ($employer) {
$q->where('employer_id', $employer->id);
})->where('status', 'waiting_for_review')
->exists();
$availabilityStatus = $pendingOfferOrApp ? 'Pending Confirmation' : ($waitingForReviewOfferOrApp ? 'Waiting for Review' : ($w->status === 'Hired' ? 'Hired' : $w->status));
$workerProfile = [
'id' => $w->id,
'name' => $w->name,
@ -1065,7 +1002,6 @@ public function getWorkerDetail(Request $request, $id)
'experience' => $w->experience,
'verified' => (bool)$w->verified,
'status' => $w->status,
'availability_status' => $availabilityStatus,
'created_at' => $w->created_at?->toISOString(),
'updated_at' => $w->updated_at?->toISOString(),
'deleted_at' => $w->deleted_at?->toISOString(),

View File

@ -1233,68 +1233,6 @@ public function declineHire(Request $request, $id)
]);
}
/**
* API for workers to check for any pending hiring requests.
* GET /api/workers/pending-hiring-request
*/
public function pendingHiringRequest(Request $request)
{
/** @var Worker $worker */
$worker = $request->attributes->get('worker');
if (!$worker) {
return response()->json([
'success' => false,
'message' => 'Unauthenticated.'
], 401);
}
// Find any pending hire requests (JobApplication in 'hire_requested' status)
$pendingApp = JobApplication::where('worker_id', $worker->id)
->where('status', 'hire_requested')
->with(['jobPost.employer.employerProfile'])
->first();
$pendingHiringRequest = null;
if ($pendingApp) {
$pendingHiringRequest = [
'id' => $pendingApp->id,
'type' => 'hire_request',
'employer_name' => $pendingApp->jobPost->employer->name ?? 'Employer',
'job_title' => $pendingApp->jobPost->title ?? 'Job Post',
'salary' => $pendingApp->jobPost->salary ?? 0,
'location' => $pendingApp->jobPost->location ?? '',
'message' => "Employer " . ($pendingApp->jobPost->employer->name ?? "Employer") . " wants to hire you for '" . ($pendingApp->jobPost->title ?? 'Job Post') . "'. Please confirm.",
];
} else {
// Find any pending direct job offers (JobOffer in 'pending' status)
$pendingOffer = \App\Models\JobOffer::where('worker_id', $worker->id)
->where('status', 'pending')
->with(['employer.employerProfile'])
->first();
if ($pendingOffer) {
$pendingHiringRequest = [
'id' => $pendingOffer->id,
'type' => 'direct_hire_request',
'employer_name' => $pendingOffer->employer->name ?? 'Employer',
'job_title' => 'Direct Sponsoring Offer',
'salary' => $pendingOffer->salary ?? 0,
'location' => $pendingOffer->location ?? '',
'message' => ($pendingOffer->employer->name ?? "Employer") . " wants to hire you. Do you want to accept this job offer?",
];
}
}
return response()->json([
'success' => true,
'data' => [
'pending_hiring_request' => $pendingHiringRequest
]
]);
}
/**
* Dispatch FCM Push Notification & In-app database notifications.
*/

View File

@ -695,44 +695,6 @@ public function getDashboard(Request $request)
];
});
// Find any pending hire requests (JobApplication in 'hire_requested' status)
$pendingApp = \App\Models\JobApplication::where('worker_id', $worker->id)
->where('status', 'hire_requested')
->with(['jobPost.employer.employerProfile'])
->first();
$pendingHiringRequest = null;
if ($pendingApp) {
$pendingHiringRequest = [
'id' => $pendingApp->id,
'type' => 'hire_request',
'employer_name' => $pendingApp->jobPost->employer->name ?? 'Employer',
'job_title' => $pendingApp->jobPost->title ?? 'Job Post',
'salary' => $pendingApp->jobPost->salary ?? 0,
'location' => $pendingApp->jobPost->location ?? '',
'message' => "Employer " . ($pendingApp->jobPost->employer->name ?? "Employer") . " wants to hire you for '" . ($pendingApp->jobPost->title ?? 'Job Post') . "'. Please confirm.",
];
} else {
// Find any pending direct job offers (JobOffer in 'pending' status)
$pendingOffer = JobOffer::where('worker_id', $worker->id)
->where('status', 'pending')
->with(['employer.employerProfile'])
->first();
if ($pendingOffer) {
$pendingHiringRequest = [
'id' => $pendingOffer->id,
'type' => 'direct_hire_request',
'employer_name' => $pendingOffer->employer->name ?? 'Employer',
'job_title' => 'Direct Sponsoring Offer',
'salary' => $pendingOffer->salary ?? 0,
'location' => $pendingOffer->location ?? '',
'message' => ($pendingOffer->employer->name ?? "Employer") . " wants to hire you. Do you want to accept this job offer?",
];
}
}
return response()->json([
'success' => true,
'data' => [
@ -742,8 +704,7 @@ public function getDashboard(Request $request)
'profile_viewed' => $profileViewed,
'currently_working_sponsor' => $currentSponsor,
'current_employer' => $currentSponsor,
'latest_charity_events_list' => $charityEvents,
'pending_hiring_request' => $pendingHiringRequest,
'latest_charity_events_list' => $charityEvents
]
], 200);

View File

@ -362,10 +362,7 @@ public function updateStatus(Request $request, $id)
return redirect()->route('employer.login');
}
$statusLower = strtolower($request->status);
$isDirectHiringAction = in_array($statusLower, ['hired', 'hire_requested']);
if (!$isDirectHiringAction && !$this->checkJobAccess($user)) {
if (!$this->checkJobAccess($user)) {
return redirect()->route('employer.subscription')
->with('error', 'Your current plan does not support the Job Apply module. Please upgrade to a Premium plan to access this feature.');
}

View File

@ -245,33 +245,14 @@ public function show($id)
];
}
$hasJobAccess = $this->checkJobAccess($user);
return Inertia::render('Employer/Messages/Show', [
'conversations' => $conversations,
'conversation' => $conversationData,
'initialMessages' => $initialMessages,
'application' => $applicationData,
'hasJobAccess' => $hasJobAccess,
]);
}
private function checkJobAccess($user)
{
if (!$user) {
return false;
}
$sub = \Illuminate\Support\Facades\DB::table('subscriptions')
->where('user_id', $user->id)
->where('status', 'active')
->latest('id')
->first();
$planId = $sub ? $sub->plan_id : 'basic';
return $planId !== 'basic';
}
public function send(Request $request, $id)
{
$user = $this->resolveCurrentUser();

View File

@ -365,16 +365,6 @@ public function show($id)
$visaStatus = $w->visa_status;
$waitingForReviewOfferOrApp = JobOffer::where('employer_id', $user ? $user->id : 0)
->where('worker_id', $w->id)
->where('status', 'waiting_for_review')
->exists() ||
\App\Models\JobApplication::where('worker_id', $w->id)
->whereHas('jobPost', function($q) use ($user) {
$q->where('employer_id', $user ? $user->id : 0);
})->where('status', 'waiting_for_review')
->exists();
$pendingOfferOrApp = JobOffer::where('employer_id', $user ? $user->id : 0)
->where('worker_id', $w->id)
->where('status', 'pending')
@ -412,7 +402,7 @@ public function show($id)
'reviews' => $reviews,
'similar_workers' => $similarWorkers,
'status' => $w->status,
'availability_status' => $pendingOfferOrApp ? 'Pending Confirmation' : ($waitingForReviewOfferOrApp ? 'Waiting for Review' : ($w->status === 'Hired' ? 'Hired' : $w->status)),
'availability_status' => $pendingOfferOrApp ? 'Pending Confirmation' : ($w->status === 'Hired' ? 'Hired' : $w->status),
'document_expiry_status' => $w->document_expiry_status,
'document_expiry_days' => $w->document_expiry_days,
'visa_expiry_date' => $w->visa_expiry_date,
@ -492,59 +482,27 @@ public function markHired(Request $request, $id)
$worker = Worker::findOrFail($id);
// Do NOT update worker status to 'Hired' directly.
// Check if there is an existing job offer for this employer and worker
$offer = JobOffer::where('employer_id', $employerId)
->where('worker_id', $worker->id)
->first();
$application = null;
if ($offer) {
$offer->update(['status' => 'pending']);
} else {
// Also check if there's an existing job application
$jobIds = \App\Models\JobPost::where('employer_id', $employerId)->pluck('id');
$application = \App\Models\JobApplication::whereIn('job_id', $jobIds)
->where('worker_id', $worker->id)
->first();
// Determine if this is first click ("Hire") or second click ("Confirm Hire")
// If they have an existing offer or application with status 'waiting_for_review', then it's the second click.
$isWaitingForReview = false;
if ($offer && $offer->status === 'waiting_for_review') {
$isWaitingForReview = true;
} elseif ($application && $application->status === 'waiting_for_review') {
$isWaitingForReview = true;
}
if (!$isWaitingForReview) {
// Step 1: Transition to 'waiting_for_review'
if ($offer) {
$offer->update(['status' => 'waiting_for_review']);
} elseif ($application) {
$application->update(['status' => 'waiting_for_review']);
$history = $application->status_history ?: [];
$history[] = [
'status' => 'waiting_for_review',
'timestamp' => now()->toIso8601String(),
'notes' => 'Candidate status moved to waiting for review.',
];
$application->update(['status_history' => $history]);
} else {
// Create a new direct offer in 'waiting_for_review' status
$offer = JobOffer::create([
'employer_id' => $employerId,
'worker_id' => $worker->id,
'work_date' => now()->format('Y-m-d'),
'location' => 'Dubai',
'salary' => $worker->salary,
'notes' => 'Direct sponsoring matching initiated.',
'status' => 'waiting_for_review',
]);
}
return back()->with('success', 'Candidate status updated to Waiting for Review.');
} else {
// Step 2: Confirm hire (Transition to pending/hire_requested and send notifications)
if ($offer) {
$offer->update(['status' => 'pending']);
} elseif ($application) {
if ($application) {
$application->update(['status' => 'hire_requested']);
$history = $application->status_history ?: [];
$history[] = [
'status' => 'hire_requested',
@ -552,6 +510,18 @@ public function markHired(Request $request, $id)
'notes' => 'Hire request initiated by employer.',
];
$application->update(['status_history' => $history]);
} else {
// Create a new direct job offer with status pending
$offer = JobOffer::create([
'employer_id' => $employerId,
'worker_id' => $worker->id,
'work_date' => now()->format('Y-m-d'),
'location' => 'Dubai',
'salary' => $worker->salary,
'notes' => 'Direct sponsoring matching initiated.',
'status' => 'pending',
]);
}
}
// Notify the worker
@ -580,5 +550,4 @@ public function markHired(Request $request, $id)
return back()->with('success', 'Hire request initiated successfully! Awaiting candidate confirmation.');
}
}
}

View File

@ -32,6 +32,10 @@ public function run(): void
SkillSeeder::class,
AdminSeeder::class,
NationalitySeeder::class,
WorkerSeeder::class,
EmployerProfileSeeder::class,
JobPostSeeder::class,
ConversationSeeder::class,
]);
}
}

View File

@ -0,0 +1,39 @@
<?php
namespace Database\Seeders;
use Illuminate\Database\Console\Seeds\WithoutModelEvents;
use Illuminate\Database\Seeder;
class EmployerProfileSeeder extends Seeder
{
/**
* Run the database seeds.
*/
public function run(): void
{
// Create Employer User
$userId = \Illuminate\Support\Facades\DB::table('users')->insertGetId([
'name' => 'Employer One',
'email' => 'employer1@example.com',
'password' => \Illuminate\Support\Facades\Hash::make('password'),
'role' => 'employer',
'created_at' => now(),
'updated_at' => now(),
]);
// Create Employer Profile
\Illuminate\Support\Facades\DB::table('employer_profiles')->insert([
'user_id' => $userId,
'company_name' => 'Al Mansoor Household',
'phone' => '+971 50 123 4567',
'verification_status' => 'approved',
'emirates_id_front' => '[DELETED_FOR_PDPL_COMPLIANCE]',
'emirates_id_back' => '[DELETED_FOR_PDPL_COMPLIANCE]',
'emirates_id_number' => '784-1990-1234567-8',
'emirates_id_expiry' => '2028-12-31',
'created_at' => now(),
'updated_at' => now(),
]);
}
}

View File

@ -0,0 +1,253 @@
<?php
namespace Database\Seeders;
use Illuminate\Database\Console\Seeds\WithoutModelEvents;
use Illuminate\Database\Seeder;
class WorkerSeeder extends Seeder
{
/**
* Run the database seeds.
*/
public function run(): void
{
$workers = [
[
'name' => 'John Doe',
'email' => 'john.doe@example.com',
'phone' => '+971 50 111 2222',
'nationality' => 'Indian',
'country' => 'United Arab Emirates',
'city' => 'Dubai',
'area' => 'Dubai Marina',
'preferred_location' => 'Dubai Marina',
'live_in_out' => 'Live-in (Stay with family)',
'language' => 'English',
'age' => 30,
'gender' => 'Male',
'salary' => 2500.00,
'availability' => 'Immediate',
'experience' => '5+ Years',
'religion' => 'Christian',
'bio' => '',
'verified' => true,
'status' => 'active',
],
[
'name' => 'Ali Hassan',
'email' => 'ali.hassan@example.com',
'phone' => '+971 50 333 4444',
'nationality' => 'Pakistani',
'country' => 'United Arab Emirates',
'city' => 'Dubai',
'area' => 'Al Barsha',
'preferred_location' => 'Al Barsha',
'live_in_out' => 'Live-out (External housing)',
'language' => 'English',
'age' => 28,
'gender' => 'Male',
'salary' => 2000.00,
'availability' => '1 Month Notice',
'experience' => '3 Years',
'religion' => 'Muslim',
'bio' => 'Skilled mason with experience in block work and plastering.',
'verified' => true,
'status' => 'active',
],
[
'name' => 'Maria Santos',
'email' => 'maria.santos@example.com',
'phone' => '+971 50 555 6666',
'nationality' => 'Filipino',
'country' => 'United Arab Emirates',
'city' => 'Yas Island',
'area' => 'Yas Island',
'preferred_location' => 'Yas Island',
'live_in_out' => 'Live-in (Stay with family)',
'language' => 'Tagalog',
'age' => 32,
'gender' => 'Female',
'salary' => 1800.00,
'availability' => 'Immediate',
'experience' => '5+ Years',
'religion' => 'Christian',
'bio' => 'Experienced nanny with 6 years working with expatriate families in Dubai. Certified in pediatric first aid.',
'verified' => true,
'status' => 'active',
],
[
'name' => 'Lakshmi Sharma',
'email' => 'lakshmi.sharma@example.com',
'phone' => '+971 50 777 8888',
'nationality' => 'Indian',
'country' => 'United Arab Emirates',
'city' => 'Abu Dhabi',
'area' => 'Khalifa City',
'preferred_location' => 'Khalifa City',
'live_in_out' => 'Live-out (External housing)',
'language' => 'Hindi',
'age' => 38,
'gender' => 'Female',
'salary' => 1600.00,
'availability' => '2 Weeks',
'experience' => '3-5 Years',
'religion' => 'Hindu',
'bio' => 'Patient caregiver specializing in elderly assistance, mobility support, and vegetarian cooking.',
'verified' => true,
'status' => 'active',
],
[
'name' => 'Siti Aminah',
'email' => 'siti.aminah@example.com',
'phone' => '+971 50 999 0000',
'nationality' => 'Indonesian',
'country' => 'United Arab Emirates',
'city' => 'Sharjah',
'area' => 'Al Nahda',
'preferred_location' => 'Al Nahda',
'live_in_out' => 'Live-in (Stay with family)',
'language' => 'Arabic',
'age' => 29,
'gender' => 'Female',
'salary' => 1400.00,
'availability' => 'Immediate',
'experience' => '3-5 Years',
'religion' => 'Muslim',
'bio' => 'Hardworking and meticulous housekeeper with excellent references from Abu Dhabi households.',
'verified' => true,
'status' => 'active',
],
[
'name' => 'Grace Osei',
'email' => 'grace.osei@example.com',
'phone' => '+971 50 222 3333',
'nationality' => 'Ghanaian',
'country' => 'Saudi Arabia',
'city' => 'Riyadh',
'area' => 'Al Olaya',
'preferred_location' => 'Al Olaya',
'live_in_out' => 'Live-out (External housing)',
'language' => 'English',
'age' => 26,
'gender' => 'Female',
'salary' => 1500.00,
'availability' => '1 Month',
'experience' => '1-2 Years',
'religion' => 'Christian',
'bio' => 'Energetic and educated nanny. Fluent in English, great with toddlers and assisting with homework.',
'verified' => false,
'status' => 'active',
],
[
'name' => 'Anoma Perera',
'email' => 'anoma.perera@example.com',
'phone' => '+971 50 444 5555',
'nationality' => 'Sri Lankan',
'country' => 'Saudi Arabia',
'city' => 'Jeddah',
'area' => 'Al Hamra',
'preferred_location' => 'Al Hamra',
'live_in_out' => 'Live-in (Stay with family)',
'language' => 'English',
'age' => 41,
'gender' => 'Female',
'salary' => 2200.00,
'availability' => 'Immediate',
'experience' => '5+ Years',
'religion' => 'Buddhist',
'bio' => 'Professional domestic cook skilled in Arabic, Continental, and Asian cuisine. Highly organized.',
'verified' => true,
'status' => 'active',
],
[
'name' => 'Ramesh Thapa',
'email' => 'ramesh.thapa@example.com',
'phone' => '+971 50 666 7777',
'nationality' => 'Nepalese',
'country' => 'Saudi Arabia',
'city' => 'Riyadh',
'area' => 'Al Yasmin',
'preferred_location' => 'Al Yasmin',
'live_in_out' => 'Live-out (External housing)',
'language' => 'English',
'age' => 36,
'gender' => 'Male',
'salary' => 2500.00,
'availability' => '2 Weeks',
'experience' => '5+ Years',
'religion' => 'Hindu',
'bio' => 'Valid UAE driving license with clean record. Familiar with all Dubai and Sharjah school routes.',
'verified' => true,
'status' => 'active',
],
];
// Dynamic Skill Resolution
$skillIds = \Illuminate\Support\Facades\DB::table('skills')->pluck('id')->toArray();
if (empty($skillIds)) {
foreach (['cooking', 'driving', 'childcare', 'cleaning', 'elderly care', 'gardening'] as $skillName) {
$skillIds[] = \Illuminate\Support\Facades\DB::table('skills')->insertGetId([
'name' => $skillName,
'created_at' => now(),
'updated_at' => now(),
]);
}
}
$sId1 = $skillIds[0] ?? 1;
$sId2 = $skillIds[1] ?? 2;
foreach ($workers as $workerData) {
// Exclude email duplicates to avoid unique constraint violations
if (\Illuminate\Support\Facades\DB::table('workers')->where('email', $workerData['email'])->exists()) {
continue;
}
$workerId = \Illuminate\Support\Facades\DB::table('workers')->insertGetId(array_merge($workerData, [
'created_at' => now(),
'updated_at' => now(),
]));
// Seed Documents
\Illuminate\Support\Facades\DB::table('worker_documents')->insert([
[
'worker_id' => $workerId,
'type' => 'passport',
'number' => 'P' . rand(100000, 999999),
'issue_date' => '2020-01-01',
'expiry_date' => '2030-01-01',
'ocr_accuracy' => 95.5,
'created_at' => now(),
'updated_at' => now(),
],
[
'worker_id' => $workerId,
'type' => 'visa',
'number' => 'V' . rand(100000, 999999),
'issue_date' => '2022-01-01',
'expiry_date' => '2025-01-01',
'ocr_accuracy' => 98.0,
'created_at' => now(),
'updated_at' => now(),
]
]);
// Seed Skills using resolved IDs
\Illuminate\Support\Facades\DB::table('worker_skills')->insert([
[
'worker_id' => $workerId,
'skill_id' => $sId1,
'created_at' => now(),
'updated_at' => now(),
],
[
'worker_id' => $workerId,
'skill_id' => $sId2,
'created_at' => now(),
'updated_at' => now(),
]
]);
}
}
}

View File

@ -160,7 +160,7 @@ export default function Register({ prefillData }) {
const newErrors = {};
if (!data.name.trim()) {
newErrors.name = 'Employer name is required.';
newErrors.name = 'Sponsor name is required.';
}
if (!data.email.trim()) {
@ -266,7 +266,7 @@ export default function Register({ prefillData }) {
return (
<div className="min-h-screen grid grid-cols-1 lg:grid-cols-12 bg-slate-50 font-sans">
<Head title="Employer Registration - Migrant Portal" />
<Head title="Sponsor Registration - Migrant Portal" />
{/* Left Brand Panel (Desktop Only) */}
<div className="hidden lg:flex lg:col-span-5 bg-[#185FA5] p-12 flex-col justify-between text-white relative overflow-hidden select-none">
@ -282,7 +282,7 @@ export default function Register({ prefillData }) {
<div className="space-y-3 pt-6">
<span className="inline-block px-3 py-1 bg-white/10 border border-white/20 rounded-full text-xs font-semibold uppercase tracking-wider">
Employer Registration
Sponsor Registration
</span>
<h1 className="text-4xl font-extrabold tracking-tight leading-tight">
Find verified domestic workers directly.
@ -326,14 +326,14 @@ export default function Register({ prefillData }) {
<div>
<h2 className="text-2xl font-bold text-gray-900 tracking-tight">Create Account</h2>
<p className="text-xs text-gray-500 mt-1">Register your employer portal to start searching</p>
<p className="text-xs text-gray-500 mt-1">Register your sponsor portal to start searching</p>
</div>
<form onSubmit={handleSubmit} noValidate className="space-y-4">
{/* Company Name */}
{/* Employer Name */}
{/* Sponsor Name */}
<div>
<label className="block text-xs font-semibold text-gray-700 mb-1.5 uppercase tracking-wider">Employer Name</label>
<label className="block text-xs font-semibold text-gray-700 mb-1.5 uppercase tracking-wider">Sponsor Name</label>
<input
type="text"
value={data.name}
@ -359,7 +359,7 @@ export default function Register({ prefillData }) {
type="email"
value={data.email}
onChange={(e) => handleInputChange('email', e.target.value)}
placeholder="employer@domain.com"
placeholder="sponsor@domain.com"
className={`w-full px-3.5 py-3 rounded-xl border text-sm focus:outline-none focus:ring-2 transition-all ${
errors.email
? 'border-red-500 focus:ring-red-500/20 focus:border-red-500'

View File

@ -32,7 +32,7 @@ import {
} from 'lucide-react';
import { toast } from 'sonner';
export default function Show({ conversation, initialMessages, conversations = [], application = null, hasJobAccess = false }) {
export default function Show({ conversation, initialMessages, conversations = [], application = null }) {
const { t } = useTranslation();
const [messages, setMessages] = useState(initialMessages || []);
const [input, setInput] = useState('');
@ -343,7 +343,7 @@ export default function Show({ conversation, initialMessages, conversations = []
</div>
<div className="flex items-center space-x-2">
{application && hasJobAccess ? (
{application ? (
<button
type="button"
onClick={() => openStatusModal(application)}

View File

@ -208,20 +208,11 @@ export default function Show({ worker }) {
router.post(`/employer/workers/${worker.id}/mark-hired`, {}, {
preserveScroll: true,
onSuccess: () => {
if (availabilityStatus !== 'Waiting for Review') {
setAvailabilityStatus('Waiting for Review');
setShowHireConfirmModal(false);
toast.success(t('moved_to_review', 'Candidate moved to review!'), {
description: t('moved_to_review_desc', 'Candidate status updated to Waiting for Review. Click Confirm Hire to proceed.')
});
} else {
setAvailabilityStatus('Pending Confirmation');
setShowHiredModal(true);
toast.success(t('hire_request_sent', 'Hiring request sent successfully!'), {
description: t('hire_request_sent_desc', 'Awaiting worker confirmation of the hiring offer.')
});
setShowHireConfirmModal(false);
}
}
});
};
@ -345,18 +336,6 @@ export default function Show({ worker }) {
<span>{t('start_direct_chat', 'Start Direct Chat')}</span>
</Link>
{/* Stage 4 Hire: Hire Worker Button */}
{availabilityStatus !== 'Pending Confirmation' && availabilityStatus !== 'Hired' && (
<button
type="button"
onClick={() => setShowHireConfirmModal(true)}
className="bg-emerald-600 hover:bg-emerald-700 border border-emerald-700 text-white px-6 py-3 rounded-xl font-bold text-sm shadow-xs transition-all flex items-center justify-center space-x-2 flex-1 sm:flex-initial cursor-pointer"
>
<UserCheck className="w-4 h-4 text-white" />
<span>{availabilityStatus === 'Waiting for Review' ? t('confirm_hire', 'Confirm Hire') : t('hire', 'Hire')}</span>
</button>
)}
{/* Stage 4 Hire: Pending Confirmation Indicator */}
{availabilityStatus === 'Pending Confirmation' && (
<div className="bg-amber-50 text-amber-700 border border-amber-200 px-6 py-3 rounded-xl font-bold text-sm flex items-center justify-center space-x-2">
@ -963,52 +942,6 @@ export default function Show({ worker }) {
</div>
)}
{/* Confirm Hire Modal Overlay */}
{showHireConfirmModal && (
<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 space-y-6 border border-slate-200 shadow-2xl">
<button
type="button"
onClick={() => setShowHireConfirmModal(false)}
className="absolute top-4 right-4 text-slate-400 hover:text-slate-600"
>
<X className="w-5 h-5" />
</button>
<div className="text-center space-y-3">
<div className="w-16 h-16 bg-blue-50 text-blue-600 rounded-full flex items-center justify-center mx-auto shadow-inner">
<UserCheck className="w-8 h-8" />
</div>
<h4 className="font-extrabold text-lg text-slate-900">
{availabilityStatus === 'Waiting for Review'
? t('confirm_hire_title', 'Confirm Sponsoring / Hiring Offer')
: t('initiate_hire_title', 'Initiate Sponsoring / Hiring')}
</h4>
<p className="text-xs text-slate-500 leading-relaxed">
{availabilityStatus === 'Waiting for Review'
? t('confirm_hire_desc', 'Are you sure you want to hire this worker? This action will send a hiring request to the worker.')
: t('initiate_hire_desc', 'Are you sure you want to initiate the hiring process for this worker? This will add them to your review list.')}
</p>
</div>
<div className="flex space-x-3 text-xs font-bold">
<button
onClick={() => setShowHireConfirmModal(false)}
className="flex-1 bg-slate-50 border border-slate-250 hover:bg-slate-100 text-slate-700 py-3 rounded-xl flex items-center justify-center"
>
<span>{t('cancel', 'Cancel')}</span>
</button>
<button
onClick={handleMarkHired}
className="flex-1 bg-emerald-600 hover:bg-emerald-700 text-white py-3 rounded-xl flex items-center justify-center"
>
<span>{t('confirm', 'Confirm')}</span>
</button>
</div>
</div>
</div>
)}
{/* Report Abuse Modal Overlay */}
{showReportModal && (
<div className="fixed inset-0 bg-slate-900/60 backdrop-blur-xs flex items-center justify-center p-4 z-50 animate-fade-in">

View File

@ -117,7 +117,6 @@
Route::post('/workers/applied-jobs/{id}/withdraw', [\App\Http\Controllers\Api\WorkerJobController::class, 'withdrawApplication']);
Route::post('/workers/applied-jobs/{id}/confirm-hire', [\App\Http\Controllers\Api\WorkerJobController::class, 'confirmHire']);
Route::post('/workers/applied-jobs/{id}/decline-hire', [\App\Http\Controllers\Api\WorkerJobController::class, 'declineHire']);
Route::get('/workers/pending-hiring-request', [\App\Http\Controllers\Api\WorkerJobController::class, 'pendingHiringRequest']);
// Review Management (Worker reviewing Employer)
Route::get('/workers/reviews', [\App\Http\Controllers\Api\WorkerReviewController::class, 'getReviews']);

View File

@ -1077,120 +1077,6 @@ public function test_direct_hiring_sends_correct_notifications_to_worker()
unlink($credentialsPath);
}
}
public function test_basic_plan_employer_can_directly_hire_without_subscription_redirect()
{
// 1. Change employer subscription to 'basic'
$this->employer->subscription()->delete();
Subscription::create([
'user_id' => $this->employer->id,
'plan_id' => 'basic',
'amount_aed' => 0.00,
'starts_at' => now(),
'expires_at' => now()->addDays(30),
'status' => 'active',
]);
// Create a job post (representing a placeholder or existing one)
$job = JobPost::create([
'employer_id' => $this->employer->id,
'title' => 'General Helper Placeholder',
'location' => 'Dubai',
'salary' => 2000,
'job_type' => 'Full Time',
'start_date' => now()->addDays(5)->format('Y-m-d'),
'description' => 'Placeholder',
'status' => 'active',
]);
// Create a job application
$application = JobApplication::create([
'worker_id' => $this->worker->id,
'job_id' => $job->id,
'status' => 'applied'
]);
// 2. Try to update status to 'Shortlisted' - should redirect to subscription
$responseShortlist = $this->actingAs($this->employer)
->withSession(['user' => $this->employer])
->post("/employer/candidates/{$application->id}/status", [
'status' => 'Shortlisted',
'notes' => 'Try to shortlist'
]);
$responseShortlist->assertRedirect(route('employer.subscription'));
// 3. Try to update status to 'Hired' - should bypass redirect and succeed
$responseHire = $this->actingAs($this->employer)
->withSession(['user' => $this->employer])
->post("/employer/candidates/{$application->id}/status", [
'status' => 'Hired',
'notes' => 'Direct hiring worker'
]);
$responseHire->assertSessionHasNoErrors();
$this->assertDatabaseHas('job_applications', [
'id' => $application->id,
'status' => 'hire_requested',
]);
}
public function test_two_step_hiring_flow()
{
// 1. Direct hiring flow via Web Controller
// First click: transition from Available to 'Waiting for Review'
$response = $this->actingAs($this->employer)
->withSession(['user' => $this->employer])
->post("/employer/workers/{$this->worker->id}/mark-hired");
$response->assertRedirect();
$this->assertDatabaseHas('job_offers', [
'employer_id' => $this->employer->id,
'worker_id' => $this->worker->id,
'status' => 'waiting_for_review',
]);
// Fetch the created job offer
$offer = \App\Models\JobOffer::where('employer_id', $this->employer->id)
->where('worker_id', $this->worker->id)
->first();
$this->assertNotNull($offer);
// Second click: transition to 'Pending Confirmation' (status 'pending') and notify worker
$response2 = $this->actingAs($this->employer)
->withSession(['user' => $this->employer])
->post("/employer/workers/{$this->worker->id}/mark-hired");
$response2->assertRedirect();
$offer->refresh();
$this->assertEquals('pending', $offer->status);
// 2. Check pending hiring request via worker API
$responseWorkerDashboard = $this->getJson('/api/workers/dashboard', [
'Authorization' => 'Bearer worker_api_token'
]);
$responseWorkerDashboard->assertStatus(200);
$responseWorkerDashboard->assertJsonPath('data.pending_hiring_request.id', $offer->id);
$responseWorkerDashboard->assertJsonPath('data.pending_hiring_request.type', 'direct_hire_request');
$responsePendingApi = $this->getJson('/api/workers/pending-hiring-request', [
'Authorization' => 'Bearer worker_api_token'
]);
$responsePendingApi->assertStatus(200);
$responsePendingApi->assertJsonPath('data.pending_hiring_request.id', $offer->id);
// 3. Worker confirms direct job offer
$responseConfirm = $this->postJson("/api/workers/offers/{$offer->id}/respond", [
'status' => 'accepted'
], [
'Authorization' => 'Bearer worker_api_token'
]);
$responseConfirm->assertStatus(200);
$offer->refresh();
$this->assertEquals('accepted', $offer->status);
$this->worker->refresh();
$this->assertEquals('Hired', $this->worker->status);
}
}

View File

@ -23,7 +23,7 @@ export default defineConfig({
port: 5173,
cors: true,
hmr: {
host: '192.168.0.241',
host: '192.168.0.217',
},
watch: {
ignored: ['**/storage/framework/views/**'],