worker api changes hiring, worker/employers api list

This commit is contained in:
mohanmd 2026-07-06 10:36:54 +05:30
parent 52b47cc2ac
commit e475a7558b
8 changed files with 555 additions and 129 deletions

View File

@ -90,30 +90,6 @@ private function formatWorker(Worker $w)
] ]
]; ];
})->toArray(), })->toArray(),
'documents' => $w->documents->map(function ($doc) {
$ocrData = $doc->ocr_data;
if ($doc->type === 'visa' && is_array($ocrData)) {
unset($ocrData['file_number']);
unset($ocrData['name']);
unset($ocrData['passport_number']);
unset($ocrData['accompanied_by']);
unset($ocrData['valid_until']);
unset($ocrData['sponsor']);
}
return [
'id' => $doc->id,
'worker_id' => $doc->worker_id,
'type' => $doc->type,
'number' => $doc->number,
'issue_date' => $doc->issue_date,
'expiry_date' => $doc->expiry_date,
'ocr_accuracy' => $doc->ocr_accuracy,
'file_path' => $doc->file_path ? url($doc->file_path) : null,
'ocr_data' => $ocrData,
'created_at' => $doc->created_at?->toISOString(),
'updated_at' => $doc->updated_at?->toISOString(),
];
})->toArray(),
'category' => [ 'category' => [
'id' => 7, 'id' => 7,
'name' => 'General Helper', 'name' => 'General Helper',
@ -440,10 +416,11 @@ public function getCandidates(Request $request)
if (!$w) return null; if (!$w) return null;
$status = 'Reviewing'; $status = 'Reviewing';
if ($app->status === 'hired') $status = 'Hired'; $dbStatusLower = strtolower($app->status ?? '');
elseif ($app->status === 'rejected') $status = 'Rejected'; if ($dbStatusLower === 'hired') $status = 'Hired';
elseif ($app->status === 'shortlisted' || $app->status === 'offer_sent') $status = 'Offer Sent'; elseif ($dbStatusLower === 'rejected') $status = 'Rejected';
elseif ($app->status === 'applied') $status = 'Reviewing'; elseif ($dbStatusLower === 'shortlisted' || $dbStatusLower === 'offer_sent' || $dbStatusLower === 'offer sent') $status = 'Offer Sent';
elseif ($dbStatusLower === 'applied') $status = 'Reviewing';
else $status = ucfirst($app->status); else $status = ucfirst($app->status);
$formattedWorker = $this->formatWorker($w); $formattedWorker = $this->formatWorker($w);
@ -465,9 +442,10 @@ public function getCandidates(Request $request)
if (!$w) return null; if (!$w) return null;
$status = 'Offer Sent'; $status = 'Offer Sent';
if ($offer->status === 'accepted') $status = 'Hired'; $dbOfferStatusLower = strtolower($offer->status ?? '');
elseif ($offer->status === 'rejected') $status = 'Rejected'; if ($dbOfferStatusLower === 'accepted' || $dbOfferStatusLower === 'hired') $status = 'Hired';
elseif ($offer->status === 'pending') $status = 'Offer Sent'; elseif ($dbOfferStatusLower === 'rejected') $status = 'Rejected';
elseif ($dbOfferStatusLower === 'pending' || $dbOfferStatusLower === 'offer sent' || $dbOfferStatusLower === 'offer_sent') $status = 'Offer Sent';
$formattedWorker = $this->formatWorker($w); $formattedWorker = $this->formatWorker($w);
@ -480,12 +458,21 @@ public function getCandidates(Request $request)
]); ]);
})->filter()->values()->toArray(); })->filter()->values()->toArray();
// Merge and sort // Merge and sort (filtering out duplicates for direct offers where already hired via application)
$mergedCandidates = array_merge($selectedWorkers, $directWorkers); $hiredAppWorkerIds = collect($selectedWorkers)
->filter(fn($w) => strtolower($w['status'] ?? '') === 'hired')
->pluck('worker_id')
->toArray();
$filteredDirectWorkers = array_filter($directWorkers, function($dw) use ($hiredAppWorkerIds) {
return $dw && !in_array($dw['worker_id'], $hiredAppWorkerIds);
});
$mergedCandidates = array_merge($selectedWorkers, $filteredDirectWorkers);
// Only display Hired candidates in the candidates pipeline (exclude active states) // Only display Hired candidates in the candidates pipeline (exclude active states)
$mergedCandidates = array_values(array_filter($mergedCandidates, function ($c) { $mergedCandidates = array_values(array_filter($mergedCandidates, function ($c) {
return $c && $c['status'] === 'Hired'; return $c && strtolower($c['status'] ?? '') === 'hired';
})); }));
// Apply filters: gender, skills, languages, nationality, availability, preferred_location, job_type, live_in_out, in_country, visa_status // Apply filters: gender, skills, languages, nationality, availability, preferred_location, job_type, live_in_out, in_country, visa_status
@ -810,6 +797,20 @@ public function hireCandidate(Request $request, $id = null)
$worker = $application->worker; $worker = $application->worker;
} }
$updatedApplication = $application; $updatedApplication = $application;
// Create/update the JobOffer record to satisfy the Worker Employer List API
JobOffer::updateOrCreate(
[
'employer_id' => $employerId,
'worker_id' => $application->worker_id,
],
[
'work_date' => $application->jobPost->start_date ?? now(),
'location' => $application->jobPost->location ?? 'Dubai',
'salary' => $application->jobPost->salary ?? 0,
'status' => 'accepted',
'notes' => $application->notes ?? 'Hired through Job Application',
]
);
} else { } else {
// Try targeting by Worker ID directly // Try targeting by Worker ID directly
$worker = Worker::find($targetId); $worker = Worker::find($targetId);
@ -841,6 +842,20 @@ public function hireCandidate(Request $request, $id = null)
if ($application) { if ($application) {
$application->update(['status' => 'hired']); $application->update(['status' => 'hired']);
$updatedApplication = $application; $updatedApplication = $application;
// Create/update the JobOffer record to satisfy the Worker Employer List API
JobOffer::updateOrCreate(
[
'employer_id' => $employerId,
'worker_id' => $application->worker_id,
],
[
'work_date' => $application->jobPost->start_date ?? now(),
'location' => $application->jobPost->location ?? 'Dubai',
'salary' => $application->jobPost->salary ?? 0,
'status' => 'accepted',
'notes' => $application->notes ?? 'Hired through Job Application',
]
);
} else { } else {
// Create a new direct job offer and accept it // Create a new direct job offer and accept it
$updatedOffer = JobOffer::create([ $updatedOffer = JobOffer::create([
@ -1049,30 +1064,6 @@ public function getWorkerDetail(Request $request, $id)
] ]
]; ];
})->toArray(), })->toArray(),
'documents' => $w->documents->map(function ($doc) {
$ocrData = $doc->ocr_data;
if ($doc->type === 'visa' && is_array($ocrData)) {
unset($ocrData['file_number']);
unset($ocrData['name']);
unset($ocrData['passport_number']);
unset($ocrData['accompanied_by']);
unset($ocrData['valid_until']);
unset($ocrData['sponsor']);
}
return [
'id' => $doc->id,
'worker_id' => $doc->worker_id,
'type' => $doc->type,
'number' => $doc->number,
'issue_date' => $doc->issue_date,
'expiry_date' => $doc->expiry_date,
'ocr_accuracy' => $doc->ocr_accuracy,
'file_path' => $doc->file_path ? url($doc->file_path) : null,
'ocr_data' => $ocrData,
'created_at' => $doc->created_at?->toISOString(),
'updated_at' => $doc->updated_at?->toISOString(),
];
})->toArray(),
]; ];
return response()->json([ return response()->json([

View File

@ -643,6 +643,20 @@ public function employerUpdateApplicationStatus(Request $request, $id)
if ($application->worker) { if ($application->worker) {
$application->worker->update(['status' => 'Hired']); $application->worker->update(['status' => 'Hired']);
} }
// Create/update the JobOffer record to satisfy the Worker Employer List API
\App\Models\JobOffer::updateOrCreate(
[
'employer_id' => $employer->id,
'worker_id' => $application->worker_id,
],
[
'work_date' => $application->jobPost->start_date ?? now(),
'location' => $application->jobPost->location ?? 'Dubai',
'salary' => $application->jobPost->salary ?? 0,
'status' => 'accepted',
'notes' => $application->notes ?? 'Hired through Job Application',
]
);
} elseif ($dbStatus === 'rejected') { } elseif ($dbStatus === 'rejected') {
if ($application->worker) { if ($application->worker) {
$application->worker->update(['status' => 'active']); $application->worker->update(['status' => 'active']);

View File

@ -4,6 +4,7 @@
use App\Http\Controllers\Controller; use App\Http\Controllers\Controller;
use App\Models\JobOffer; use App\Models\JobOffer;
use App\Models\JobApplication;
use App\Models\Worker; use App\Models\Worker;
use App\Models\WorkerDocument; use App\Models\WorkerDocument;
use App\Models\ProfileView; use App\Models\ProfileView;
@ -587,13 +588,29 @@ public function getDashboard(Request $request)
// 3. Currently working sponsor/employer // 3. Currently working sponsor/employer
$currentSponsor = null; $currentSponsor = null;
$emp = null;
$hiredAt = null;
$acceptedOffer = JobOffer::where('worker_id', $worker->id) $acceptedOffer = JobOffer::where('worker_id', $worker->id)
->where('status', 'accepted') ->whereIn(DB::raw('LOWER(status)'), ['accepted', 'hired'])
->with(['employer.employerProfile', 'employer.employerReviews.worker']) ->with(['employer.employerProfile', 'employer.employerReviews.worker'])
->first(); ->first();
if ($acceptedOffer && $acceptedOffer->employer) { if ($acceptedOffer) {
$emp = $acceptedOffer->employer; $emp = $acceptedOffer->employer;
$hiredAt = $acceptedOffer->updated_at;
} else {
$hiredApplication = \App\Models\JobApplication::where('worker_id', $worker->id)
->whereIn(DB::raw('LOWER(status)'), ['hired'])
->with(['jobPost.employer.employerProfile', 'jobPost.employer.employerReviews.worker'])
->first();
if ($hiredApplication && $hiredApplication->jobPost && $hiredApplication->jobPost->employer) {
$emp = $hiredApplication->jobPost->employer;
$hiredAt = $hiredApplication->updated_at;
}
}
if ($emp) {
$empProfile = $emp->employerProfile; $empProfile = $emp->employerProfile;
$currentSponsor = [ $currentSponsor = [
'id' => $emp->id, 'id' => $emp->id,
@ -602,9 +619,9 @@ public function getDashboard(Request $request)
'nationality' => $empProfile->nationality ?? 'UAE', 'nationality' => $empProfile->nationality ?? 'UAE',
'city' => $empProfile->city ?? 'Dubai', 'city' => $empProfile->city ?? 'Dubai',
'email' => $emp->email, 'email' => $emp->email,
'phone' => $emp->phone, 'phone' => $empProfile->phone ?? $emp->phone,
'hired_at' => $acceptedOffer->updated_at->toIso8601String(), 'hired_at' => $hiredAt ? $hiredAt->toIso8601String() : null,
'hired_at_formatted' => $acceptedOffer->updated_at->format('M d, Y'), 'hired_at_formatted' => $hiredAt ? $hiredAt->format('M d, Y') : null,
'rating' => $emp->rating, 'rating' => $emp->rating,
'reviews_count' => $emp->reviews_count, 'reviews_count' => $emp->reviews_count,
'reviews' => $emp->employerReviews, 'reviews' => $emp->employerReviews,
@ -687,45 +704,22 @@ public function getEmployers(Request $request)
$worker = $request->attributes->get('worker'); $worker = $request->attributes->get('worker');
try { try {
$query = JobOffer::where('worker_id', $worker->id) // Fetch JobOffers
->with(['employer.employerProfile', 'employer.employerReviews.worker']); $offers = JobOffer::where('worker_id', $worker->id)
->with(['employer.employerProfile', 'employer.employerReviews.worker'])
->get();
// Filtering by status // Fetch hired JobApplications
if ($request->filled('status')) { $applications = JobApplication::where('worker_id', $worker->id)
$statusInput = strtolower($request->status); ->where('status', 'hired')
if ($statusInput === 'active') { ->with(['jobPost.employer.employerProfile', 'jobPost.employer.employerReviews.worker'])
$query->where('status', 'accepted'); ->get();
} else {
$query->where('status', $statusInput);
}
}
// Sorting // Map JobOffers
$sortBy = $request->input('sort_by', 'joining_date'); $mappedOffers = $offers->map(function ($offer) {
$sortOrder = strtolower($request->input('sort_order', 'desc')) === 'asc' ? 'asc' : 'desc';
$sortColumn = 'work_date';
if ($sortBy === 'salary') {
$sortColumn = 'salary';
} elseif ($sortBy === 'status') {
$sortColumn = 'status';
} elseif ($sortBy === 'created_at') {
$sortColumn = 'created_at';
}
// Always display the current active employer (status = accepted) at the top of the list,
// followed by other employers ordered by the chosen sort column.
$query->orderByRaw("CASE WHEN status = 'accepted' THEN 0 ELSE 1 END ASC")
->orderBy($sortColumn, $sortOrder);
$perPage = (int) $request->input('per_page', 15);
$paginator = $query->paginate($perPage);
$employers = collect($paginator->items())->map(function ($offer) {
$emp = $offer->employer; $emp = $offer->employer;
$empProfile = $emp ? $emp->employerProfile : null; $empProfile = $emp ? $emp->employerProfile : null;
// Map 'accepted' -> 'Active' (and others to Title Case for nice presentation)
$statusFormatted = $offer->status; $statusFormatted = $offer->status;
if (strtolower($offer->status) === 'accepted') { if (strtolower($offer->status) === 'accepted') {
$statusFormatted = 'Active'; $statusFormatted = 'Active';
@ -736,10 +730,13 @@ public function getEmployers(Request $request)
return [ return [
'offer_id' => $offer->id, 'offer_id' => $offer->id,
'joining_date' => $offer->work_date ? $offer->work_date->format('Y-m-d') : null, 'joining_date' => $offer->work_date ? $offer->work_date->format('Y-m-d') : null,
'joining_date_raw' => $offer->work_date,
'location' => $offer->location, 'location' => $offer->location,
'salary' => (int) $offer->salary, 'salary' => (int) $offer->salary,
'status' => $statusFormatted, 'status' => $statusFormatted,
'status_raw' => $offer->status,
'notes' => $offer->notes, 'notes' => $offer->notes,
'created_at' => $offer->created_at,
'employer' => $emp ? [ 'employer' => $emp ? [
'id' => $emp->id, 'id' => $emp->id,
'name' => $emp->name, 'name' => $emp->name,
@ -755,15 +752,120 @@ public function getEmployers(Request $request)
]; ];
}); });
// Map JobApplications
$mappedApps = $applications->map(function ($app) {
$job = $app->jobPost;
$emp = $job ? $job->employer : null;
$empProfile = $emp ? $emp->employerProfile : null;
return [
'offer_id' => $app->id,
'joining_date' => $app->updated_at ? $app->updated_at->format('Y-m-d') : null,
'joining_date_raw' => $app->updated_at,
'location' => $job->location ?? null,
'salary' => $job ? (int) $job->salary : 0,
'status' => 'Active',
'status_raw' => 'accepted',
'notes' => $app->notes,
'created_at' => $app->created_at,
'employer' => $emp ? [
'id' => $emp->id,
'name' => $emp->name,
'email' => $emp->email,
'phone' => $emp->phone,
'company_name' => $empProfile->company_name ?? 'Private Sponsor',
'city' => $empProfile->city ?? null,
'nationality' => $empProfile->nationality ?? null,
'rating' => $emp->rating,
'reviews_count' => $emp->reviews_count,
'reviews' => $emp->employerReviews,
] : null
];
});
// Merge and de-duplicate by employer id
$merged = $mappedOffers->concat($mappedApps)->unique(function ($item) {
return $item['employer']['id'] ?? null;
})->values();
// Filtering by status
if ($request->filled('status')) {
$statusInput = strtolower($request->status);
$merged = $merged->filter(function ($item) use ($statusInput) {
$rawStatus = strtolower($item['status_raw'] ?? '');
if ($statusInput === 'active') {
return $rawStatus === 'accepted' || $rawStatus === 'hired';
}
return $rawStatus === $statusInput;
});
}
// Sorting
$sortBy = $request->input('sort_by', 'joining_date');
$sortOrder = strtolower($request->input('sort_order', 'desc')) === 'asc' ? 'asc' : 'desc';
$merged = $merged->sort(function ($a, $b) use ($sortBy, $sortOrder) {
// Priority: status = 'Active' (accepted/hired) comes first
$aActive = $a['status'] === 'Active' ? 0 : 1;
$bActive = $b['status'] === 'Active' ? 0 : 1;
if ($aActive !== $bActive) {
return $aActive <=> $bActive;
}
$valA = null;
$valB = null;
if ($sortBy === 'salary') {
$valA = $a['salary'];
$valB = $b['salary'];
} elseif ($sortBy === 'status') {
$valA = $a['status'];
$valB = $b['status'];
} elseif ($sortBy === 'created_at') {
$valA = $a['created_at'];
$valB = $b['created_at'];
} else {
$valA = $a['joining_date_raw'];
$valB = $b['joining_date_raw'];
}
if ($valA == $valB) {
return 0;
}
if ($sortOrder === 'asc') {
return $valA > $valB ? 1 : -1;
} else {
return $valA < $valB ? 1 : -1;
}
});
// Pagination
$total = $merged->count();
$perPage = (int) $request->input('per_page', 15);
$currentPage = (int) $request->input('page', 1);
$lastPage = max(1, (int) ceil($total / $perPage));
$pageItems = $merged->slice(($currentPage - 1) * $perPage, $perPage)->values();
// Clean up temporary sorting/filtering keys
$employers = $pageItems->map(function ($item) {
unset($item['joining_date_raw']);
unset($item['status_raw']);
unset($item['created_at']);
return $item;
});
return response()->json([ return response()->json([
'success' => true, 'success' => true,
'data' => [ 'data' => [
'employers' => $employers, 'employers' => $employers,
'pagination' => [ 'pagination' => [
'total' => $paginator->total(), 'total' => $total,
'per_page' => $paginator->perPage(), 'per_page' => $perPage,
'current_page' => $paginator->currentPage(), 'current_page' => $currentPage,
'last_page' => $paginator->lastPage(), 'last_page' => $lastPage,
] ]
] ]
], 200); ], 200);
@ -976,7 +1078,22 @@ public function getEmployerProfile(Request $request, $id)
->where('worker_id', $worker->id) ->where('worker_id', $worker->id)
->exists(); ->exists();
if ($hasOffer || $hasApplication || $hasConversation) { // If the worker has 'Hired' status (case-insensitive) and is hired by this employer
$isHiredByThisEmployer = false;
if (strtolower($worker->status ?? '') === 'hired') {
$isHiredByThisEmployer = \App\Models\JobOffer::where('employer_id', $employer->id)
->where('worker_id', $worker->id)
->whereIn(DB::raw('LOWER(status)'), ['accepted', 'hired'])
->exists() ||
\App\Models\JobApplication::where('worker_id', $worker->id)
->whereHas('jobPost', function ($query) use ($employer) {
$query->where('employer_id', $employer->id);
})
->where(DB::raw('LOWER(status)'), 'hired')
->exists();
}
if ($hasOffer || $hasApplication || $hasConversation || $isHiredByThisEmployer) {
$hasAccess = true; $hasAccess = true;
} }

View File

@ -55,10 +55,11 @@ public function index(Request $request)
// Map DB status to UI friendly status // Map DB status to UI friendly status
$status = 'Reviewing'; $status = 'Reviewing';
if ($app->status === 'hired') $status = 'Hired'; $dbStatusLower = strtolower($app->status ?? '');
elseif ($app->status === 'rejected') $status = 'Rejected'; if ($dbStatusLower === 'hired') $status = 'Hired';
elseif ($app->status === 'shortlisted' || $app->status === 'offer_sent') $status = 'Offer Sent'; elseif ($dbStatusLower === 'rejected') $status = 'Rejected';
elseif ($app->status === 'applied') $status = 'Reviewing'; elseif ($dbStatusLower === 'shortlisted' || $dbStatusLower === 'offer_sent' || $dbStatusLower === 'offer sent') $status = 'Offer Sent';
elseif ($dbStatusLower === 'applied') $status = 'Reviewing';
else $status = ucfirst($app->status); else $status = ucfirst($app->status);
return [ return [
@ -83,15 +84,27 @@ public function index(Request $request)
->with('worker') ->with('worker')
->get(); ->get();
$directWorkers = $directOffers->map(function ($offer) { // Get worker IDs of all hired job applications for this employer to prevent duplication
$hiredAppWorkerIds = collect($applications)
->filter(fn($app) => strtolower($app->status ?? '') === 'hired')
->pluck('worker_id')
->toArray();
$directWorkers = $directOffers->map(function ($offer) use ($hiredAppWorkerIds) {
$w = $offer->worker; $w = $offer->worker;
if (!$w) return null; if (!$w) return null;
// Avoid duplicate hired entries if worker was hired via standard application
$dbOfferStatusLower = strtolower($offer->status ?? '');
if (($dbOfferStatusLower === 'accepted' || $dbOfferStatusLower === 'hired') && in_array($w->id, $hiredAppWorkerIds)) {
return null;
}
// Map DB offer status to UI friendly status // Map DB offer status to UI friendly status
$status = 'Offer Sent'; $status = 'Offer Sent';
if ($offer->status === 'accepted') $status = 'Hired'; if ($dbOfferStatusLower === 'accepted' || $dbOfferStatusLower === 'hired') $status = 'Hired';
elseif ($offer->status === 'rejected') $status = 'Rejected'; elseif ($dbOfferStatusLower === 'rejected') $status = 'Rejected';
elseif ($offer->status === 'pending') $status = 'Offer Sent'; elseif ($dbOfferStatusLower === 'pending' || $dbOfferStatusLower === 'offer sent' || $dbOfferStatusLower === 'offer_sent') $status = 'Offer Sent';
return [ return [
'id' => 'offer_' . $offer->id, // Unique string key prefix to prevent clashes 'id' => 'offer_' . $offer->id, // Unique string key prefix to prevent clashes
@ -115,7 +128,7 @@ public function index(Request $request)
// Only display Hired candidates in the candidates pipeline // Only display Hired candidates in the candidates pipeline
$mergedCandidates = array_values(array_filter($mergedCandidates, function ($w) { $mergedCandidates = array_values(array_filter($mergedCandidates, function ($w) {
return $w && $w['status'] === 'Hired'; return $w && strtolower($w['status'] ?? '') === 'hired';
})); }));
// Apply filters: preferred_location, job_type, live_in_out, nationality, in_country, visa_status // Apply filters: preferred_location, job_type, live_in_out, nationality, in_country, visa_status
@ -411,6 +424,20 @@ public function updateStatus(Request $request, $id)
if ($app->worker) { if ($app->worker) {
$app->worker->update(['status' => 'Hired']); $app->worker->update(['status' => 'Hired']);
} }
// Create/update the JobOffer record to satisfy the Worker Employer List API
JobOffer::updateOrCreate(
[
'employer_id' => $user->id,
'worker_id' => $app->worker_id,
],
[
'work_date' => $app->jobPost->start_date ?? now(),
'location' => $app->jobPost->location ?? 'Dubai',
'salary' => $app->jobPost->salary ?? 0,
'status' => 'accepted',
'notes' => $app->notes ?? 'Hired through Job Application',
]
);
} elseif ($dbStatus === 'rejected') { } elseif ($dbStatus === 'rejected') {
if ($app->worker) { if ($app->worker) {
$app->worker->update(['status' => 'active']); $app->worker->update(['status' => 'active']);

View File

@ -78,7 +78,7 @@ export default function SelectedCandidates({
// Apply Client-Side Filters // Apply Client-Side Filters
const filteredWorkers = useMemo(() => { const filteredWorkers = useMemo(() => {
let list = (selectedWorkers || []).filter(w => w.status !== 'Searching'); let list = (workers || []).filter(w => w.status !== 'Searching');
if (searchTerm.trim() !== '') { if (searchTerm.trim() !== '') {
const query = searchTerm.toLowerCase(); const query = searchTerm.toLowerCase();
@ -134,7 +134,7 @@ export default function SelectedCandidates({
} }
return list; return list;
}, [selectedWorkers, searchTerm, filterProfession, filterLocation, filterJobType, filterAccommodation, filterNationalities, filterGender, filterInCountry, filterVisaType, filterSkills, filterLanguages, maxSalary]); }, [workers, searchTerm, filterProfession, filterLocation, filterJobType, filterAccommodation, filterNationalities, filterGender, filterInCountry, filterVisaType, filterSkills, filterLanguages, maxSalary]);
const resetFilters = () => { const resetFilters = () => {
setFilterProfession('All Professions'); setFilterProfession('All Professions');

View File

@ -541,11 +541,8 @@ public function test_worker_list_api_has_full_details_and_pagination()
$this->assertCount(1, $matchingWorker['skills']); $this->assertCount(1, $matchingWorker['skills']);
$this->assertEquals('Ironing', $matchingWorker['skills'][0]['name']); $this->assertEquals('Ironing', $matchingWorker['skills'][0]['name']);
// Check documents object array // Check documents object array is not present
$this->assertIsArray($matchingWorker['documents']); $this->assertArrayNotHasKey('documents', $matchingWorker);
$this->assertCount(2, $matchingWorker['documents']);
$this->assertEquals('passport', $matchingWorker['documents'][0]['type']);
$this->assertEquals('visa', $matchingWorker['documents'][1]['type']);
} }
public function test_worker_detail_api_has_full_details_without_religion_and_cleaned_ocr_data() public function test_worker_detail_api_has_full_details_without_religion_and_cleaned_ocr_data()
@ -618,16 +615,7 @@ public function test_worker_detail_api_has_full_details_without_religion_and_cle
// Assert religion is removed // Assert religion is removed
$this->assertArrayNotHasKey('religion', $wData); $this->assertArrayNotHasKey('religion', $wData);
// Check documents object array // Check documents object array is not present
$this->assertIsArray($wData['documents']); $this->assertArrayNotHasKey('documents', $wData);
$this->assertCount(2, $wData['documents']);
// Assert visa ocr_data is cleaned up
$visaDoc = collect($wData['documents'])->firstWhere('type', 'visa');
$this->assertNotNull($visaDoc);
$this->assertArrayNotHasKey('sponsor', $visaDoc['ocr_data']);
$this->assertArrayNotHasKey('valid_until', $visaDoc['ocr_data']);
$this->assertEquals('some sponsor name', $visaDoc['ocr_data']['sponsor_name']);
$this->assertEquals('2024-02-15', $visaDoc['ocr_data']['expiry_date']);
} }
} }

View File

@ -404,4 +404,196 @@ public function test_worker_can_view_employer_profile_without_permissions_masks_
$response->assertJsonPath('data.employer.email', null); // Masked/null since no connection exists $response->assertJsonPath('data.employer.email', null); // Masked/null since no connection exists
$response->assertJsonPath('data.employer.phone', null); $response->assertJsonPath('data.employer.phone', null);
} }
public function test_worker_hired_via_job_application_appears_in_employers_list()
{
// 1. Create a worker
$worker = Worker::create([
'name' => 'Hired Worker',
'email' => 'hiredworker@example.com',
'phone' => '+971501234567',
'language' => 'HI',
'password' => bcrypt('password'),
'nationality' => 'Indian',
'age' => 25,
'salary' => 1500,
'availability' => 'Immediate',
'experience' => 'Not Specified',
'religion' => 'Not Specified',
'bio' => 'Test',
'verified' => true,
'status' => 'active',
'api_token' => 'worker-hired-token',
]);
WorkerDocument::create([
'worker_id' => $worker->id,
'type' => 'passport',
'number' => '123456',
'file_path' => 'passports/test.pdf',
]);
// 2. Create employer with active subscription
$employer = User::create([
'name' => 'Premium Employer',
'email' => 'premium_emp@example.com',
'password' => bcrypt('password'),
'role' => 'employer',
'subscription_status' => 'active',
'api_token' => 'premium_employer_token',
]);
EmployerProfile::create([
'user_id' => $employer->id,
'company_name' => 'Premium Corp Ltd',
'nationality' => 'UAE',
'city' => 'Abu Dhabi',
]);
\App\Models\Subscription::create([
'user_id' => $employer->id,
'plan_id' => 'premium',
'amount_aed' => 100.00,
'starts_at' => now(),
'expires_at' => now()->addDays(30),
'status' => 'active',
]);
// 3. Create a JobPost
$job = \App\Models\JobPost::create([
'employer_id' => $employer->id,
'title' => 'Housekeeper Needed',
'workers_needed' => 1,
'location' => 'Abu Dhabi',
'salary' => 2000,
'job_type' => 'Full Time',
'start_date' => now()->addDays(5),
'description' => 'Test job',
'requirements' => 'None',
'status' => 'active',
]);
// 4. Create JobApplication
$application = \App\Models\JobApplication::create([
'job_id' => $job->id,
'worker_id' => $worker->id,
'status' => 'applied',
'notes' => 'Looking forward to work',
]);
// Verify employer list is empty for the worker initially
$response = $this->withHeaders([
'Authorization' => 'Bearer worker-hired-token',
])->getJson('/api/workers/employers');
$response->assertStatus(200);
$response->assertJsonCount(0, 'data.employers');
// 5. Employer hires candidate via API
$response = $this->postJson("/api/employers/applications/{$application->id}/status", [
'status' => 'hired',
'notes' => 'Hired and approved'
], [
'Authorization' => 'Bearer premium_employer_token'
]);
$response->assertStatus(200);
// Verify that the JobOffer with accepted status has been created
$this->assertDatabaseHas('job_offers', [
'employer_id' => $employer->id,
'worker_id' => $worker->id,
'status' => 'accepted',
]);
// 6. Hit the worker employers API and assert that the employer is now listed
$response = $this->withHeaders([
'Authorization' => 'Bearer worker-hired-token',
])->getJson('/api/workers/employers');
$response->assertStatus(200);
$response->assertJsonCount(1, 'data.employers');
$response->assertJsonPath('data.employers.0.employer.name', 'Premium Employer');
$response->assertJsonPath('data.employers.0.employer.company_name', 'Premium Corp Ltd');
$response->assertJsonPath('data.employers.0.status', 'Active');
}
public function test_worker_hired_via_job_application_appears_without_job_offer()
{
// 1. Create a worker
$worker = Worker::create([
'name' => 'Hired Worker Legacy',
'email' => 'legacyworker@example.com',
'phone' => '+971501234560',
'language' => 'HI',
'password' => bcrypt('password'),
'nationality' => 'Indian',
'age' => 25,
'salary' => 1500,
'availability' => 'Immediate',
'experience' => 'Not Specified',
'religion' => 'Not Specified',
'bio' => 'Test',
'verified' => true,
'status' => 'active',
'api_token' => 'worker-legacy-token',
]);
WorkerDocument::create([
'worker_id' => $worker->id,
'type' => 'passport',
'number' => '123456',
'file_path' => 'passports/test.pdf',
]);
// 2. Create employer
$employer = User::create([
'name' => 'Legacy Employer',
'email' => 'legacy_emp@example.com',
'password' => bcrypt('password'),
'role' => 'employer',
'subscription_status' => 'active',
'api_token' => 'legacy_employer_token',
]);
EmployerProfile::create([
'user_id' => $employer->id,
'company_name' => 'Legacy Corp Ltd',
'nationality' => 'UAE',
'city' => 'Dubai',
]);
// 3. Create a JobPost
$job = \App\Models\JobPost::create([
'employer_id' => $employer->id,
'title' => 'Housekeeper Needed Legacy',
'workers_needed' => 1,
'location' => 'Dubai',
'salary' => 2000,
'job_type' => 'Full Time',
'start_date' => now()->addDays(5),
'description' => 'Test job',
'requirements' => 'None',
'status' => 'active',
]);
// 4. Create JobApplication with status 'hired' directly, and NO JobOffer
$application = \App\Models\JobApplication::create([
'job_id' => $job->id,
'worker_id' => $worker->id,
'status' => 'hired',
'notes' => 'Hired directly in DB previously',
]);
// 5. Hit the worker employers API and assert that the employer is listed
$response = $this->withHeaders([
'Authorization' => 'Bearer worker-legacy-token',
])->getJson('/api/workers/employers');
$response->assertStatus(200);
$response->assertJsonCount(1, 'data.employers');
$response->assertJsonPath('data.employers.0.employer.name', 'Legacy Employer');
$response->assertJsonPath('data.employers.0.employer.company_name', 'Legacy Corp Ltd');
$response->assertJsonPath('data.employers.0.status', 'Active');
}
} }

View File

@ -507,4 +507,101 @@ public function test_job_hiring_workflow_enhancements()
'id' => $app->id 'id' => $app->id
]); ]);
} }
public function test_hired_worker_visibility_and_profile_access()
{
// Create EmployerProfile with a phone number
\App\Models\EmployerProfile::create([
'user_id' => $this->employer->id,
'company_name' => 'Premium Sponsoring Group',
'phone' => '971501234567',
'city' => 'Dubai',
'nationality' => 'UAE',
]);
// 1. Create a job post
$job = JobPost::create([
'employer_id' => $this->employer->id,
'title' => 'Special Hired Job',
'location' => 'Dubai Jumeirah',
'salary' => 4500,
'workers_needed' => 1,
'job_type' => 'Full Time',
'start_date' => now()->addDays(5)->format('Y-m-d'),
'description' => 'A special job details description',
'status' => 'active',
]);
// 2. Worker applies for the job
$response = $this->postJson("/api/workers/jobs/{$job->id}/apply", [], [
'Authorization' => 'Bearer worker_api_token'
]);
$response->assertStatus(201);
$app = JobApplication::first();
// 3. Employer hires the worker by setting status to 'hired' (lowercase, as in the database)
$response = $this->postJson("/api/employers/applications/{$app->id}/status", [
'status' => 'hired',
'notes' => 'Hired through job flow'
], [
'Authorization' => 'Bearer employer_api_token'
]);
$response->assertStatus(200);
// Verify status updates
$this->assertDatabaseHas('job_applications', [
'id' => $app->id,
'status' => 'hired',
]);
$this->assertDatabaseHas('workers', [
'id' => $this->worker->id,
'status' => 'Hired',
]);
// 4. Verify candidate is visible in Hired list (web: CandidateController::index)
$response = $this->actingAs($this->employer)
->withSession(['user' => $this->employer])
->get('/employer/candidates');
$response->assertStatus(200);
// Extract the Inertia prop 'selectedWorkers' to check
$inertiaData = $response->original->getData();
$selectedWorkers = $inertiaData['page']['props']['selectedWorkers'] ?? [];
$hiredList = array_values(array_filter($selectedWorkers, function ($w) {
return $w['worker_id'] === $this->worker->id;
}));
$this->assertCount(1, $hiredList);
$this->assertEquals('Hired', $hiredList[0]['status']);
// 5. Verify candidate is visible in Hired list (API: EmployerWorkerController::getCandidates)
$response = $this->getJson('/api/employers/candidates', [
'Authorization' => 'Bearer employer_api_token'
]);
$response->assertStatus(200);
$response->assertJsonPath('success', true);
// Find the worker in candidates array
$candidates = $response->json('data.candidates');
$hiredApiList = array_values(array_filter($candidates, function ($c) {
return $c['worker_id'] === $this->worker->id;
}));
$this->assertCount(1, $hiredApiList);
$this->assertEquals('Hired', $hiredApiList[0]['status']);
// 6. Verify hired worker can view employer profile details bypassing subscription limits (WorkerProfileController::getEmployerProfile)
$response = $this->getJson("/api/workers/employers/{$this->employer->id}", [
'Authorization' => 'Bearer worker_api_token'
]);
$response->assertStatus(200);
$response->assertJsonPath('success', true);
$response->assertJsonPath('data.employer.email', 'employer@example.com');
$response->assertJsonPath('data.employer.phone', '971501234567'); // should be exposed/visible because worker is hired
// 7. Verify hired worker dashboard shows correct currently working sponsor
$response = $this->getJson('/api/workers/dashboard', [
'Authorization' => 'Bearer worker_api_token'
]);
$response->assertStatus(200);
$response->assertJsonPath('success', true);
$response->assertJsonPath('data.current_employer.id', $this->employer->id);
$response->assertJsonPath('data.currently_working_sponsor.id', $this->employer->id);
}
} }