mohan #5
362
app/Http/Controllers/Api/EmployerWorkerController.php
Normal file
362
app/Http/Controllers/Api/EmployerWorkerController.php
Normal file
@ -0,0 +1,362 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Controllers\Api;
|
||||||
|
|
||||||
|
use App\Http\Controllers\Controller;
|
||||||
|
use Illuminate\Http\Request;
|
||||||
|
use App\Models\User;
|
||||||
|
use App\Models\Worker;
|
||||||
|
use App\Models\WorkerCategory;
|
||||||
|
use App\Models\Shortlist;
|
||||||
|
use App\Models\JobOffer;
|
||||||
|
use App\Models\JobApplication;
|
||||||
|
use App\Models\JobPost;
|
||||||
|
use Illuminate\Support\Facades\DB;
|
||||||
|
use Illuminate\Support\Facades\Validator;
|
||||||
|
|
||||||
|
class EmployerWorkerController extends Controller
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Helper to map worker DB models to API presentation format.
|
||||||
|
*/
|
||||||
|
private function formatWorker(Worker $w)
|
||||||
|
{
|
||||||
|
// Map languages with country names
|
||||||
|
$langs = ($w->id % 2 === 0) ? ['English', 'Arabic'] : (($w->id % 3 === 0) ? ['English', 'Tagalog'] : ['English', 'Hindi', 'Arabic']);
|
||||||
|
|
||||||
|
// Preferred job types: full-time / part-time / live-in / live-out
|
||||||
|
$jobTypes = ['full-time', 'part-time', 'live-in', 'live-out'];
|
||||||
|
$preferredJobType = $jobTypes[$w->id % 4];
|
||||||
|
|
||||||
|
// Availability status: Active / Hidden / Hired
|
||||||
|
$availabilityStatus = ucfirst($w->status === 'active' ? 'Active' : ($w->status === 'hidden' ? 'Hidden' : ($w->status === 'Hired' ? 'Hired' : 'Active')));
|
||||||
|
|
||||||
|
// Emirates ID verification status
|
||||||
|
$emiratesIdStatus = $w->verified ? 'Emirates ID Verified' : 'EID Vetting Pending';
|
||||||
|
|
||||||
|
// Exact Skills mapping: cooking, driving, childcare, cleaning, elderly care, gardening
|
||||||
|
$skillsList = ['cooking', 'driving', 'childcare', 'cleaning', 'elderly care', 'gardening'];
|
||||||
|
$mappedSkills = [
|
||||||
|
$skillsList[$w->id % 6],
|
||||||
|
$skillsList[($w->id + 2) % 6]
|
||||||
|
];
|
||||||
|
|
||||||
|
// Visa status
|
||||||
|
$visaStatusesList = ['Residence Visa', 'Tourist Visa', 'Employment Visa', 'Cancelled Visa', 'Own Visa'];
|
||||||
|
$visaStatus = $visaStatusesList[$w->id % 5];
|
||||||
|
|
||||||
|
// Optional profile photos
|
||||||
|
$photos = [
|
||||||
|
'https://images.unsplash.com/photo-1573496359142-b8d87734a5a2?auto=format&fit=crop&q=80&w=200',
|
||||||
|
'https://images.unsplash.com/photo-1534528741775-53994a69daeb?auto=format&fit=crop&q=80&w=200',
|
||||||
|
'https://images.unsplash.com/photo-1544005313-94ddf0286df2?auto=format&fit=crop&q=80&w=200',
|
||||||
|
null
|
||||||
|
];
|
||||||
|
$photo = $photos[$w->id % 4];
|
||||||
|
|
||||||
|
$rating = 4.0 + (($w->id * 3) % 10) / 10.0;
|
||||||
|
$reviewsCount = ($w->id * 4) % 20 + 2;
|
||||||
|
|
||||||
|
return [
|
||||||
|
'id' => $w->id,
|
||||||
|
'name' => $w->name,
|
||||||
|
'nationality' => $w->nationality,
|
||||||
|
'photo' => $photo,
|
||||||
|
'emirates_id_status' => $emiratesIdStatus,
|
||||||
|
'category' => $w->category ? $w->category->name : 'Domestic Worker',
|
||||||
|
'skills' => $mappedSkills,
|
||||||
|
'availability_status' => $availabilityStatus,
|
||||||
|
'visa_status' => $visaStatus,
|
||||||
|
'experience' => $w->experience,
|
||||||
|
'salary' => (int)$w->salary,
|
||||||
|
'religion' => $w->religion,
|
||||||
|
'languages' => $langs,
|
||||||
|
'age' => $w->age,
|
||||||
|
'verified' => (bool)$w->verified,
|
||||||
|
'preferred_job_type' => $preferredJobType,
|
||||||
|
'bio' => $w->bio,
|
||||||
|
'rating' => $rating,
|
||||||
|
'reviews_count' => $reviewsCount,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 1. GET /api/employers/workers
|
||||||
|
* List of workers available for hiring.
|
||||||
|
*/
|
||||||
|
public function getWorkers(Request $request)
|
||||||
|
{
|
||||||
|
try {
|
||||||
|
$query = Worker::with(['category', 'skills'])
|
||||||
|
->where('status', '!=', 'Hired')
|
||||||
|
->where('status', '!=', 'hidden');
|
||||||
|
|
||||||
|
// Apply search filter if provided
|
||||||
|
if ($request->filled('search')) {
|
||||||
|
$search = $request->search;
|
||||||
|
$query->where(function($q) use ($search) {
|
||||||
|
$q->where('name', 'like', "%{$search}%")
|
||||||
|
->orWhere('nationality', 'like', "%{$search}%")
|
||||||
|
->orWhere('religion', 'like', "%{$search}%");
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Apply category filter if provided
|
||||||
|
if ($request->filled('category_id')) {
|
||||||
|
$query->where('category_id', $request->category_id);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Apply nationality filter if provided
|
||||||
|
if ($request->filled('nationality')) {
|
||||||
|
$query->where('nationality', $request->nationality);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Apply salary filters
|
||||||
|
if ($request->filled('min_salary')) {
|
||||||
|
$query->where('salary', '>=', $request->min_salary);
|
||||||
|
}
|
||||||
|
if ($request->filled('max_salary')) {
|
||||||
|
$query->where('salary', '<=', $request->max_salary);
|
||||||
|
}
|
||||||
|
|
||||||
|
$dbWorkers = $query->latest()->get();
|
||||||
|
|
||||||
|
$formattedWorkers = $dbWorkers->map(function ($w) {
|
||||||
|
return $this->formatWorker($w);
|
||||||
|
});
|
||||||
|
|
||||||
|
return response()->json([
|
||||||
|
'success' => true,
|
||||||
|
'data' => [
|
||||||
|
'workers' => $formattedWorkers
|
||||||
|
]
|
||||||
|
], 200);
|
||||||
|
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
logger()->error('Mobile API Employer Get Workers Failure: ' . $e->getMessage());
|
||||||
|
|
||||||
|
return response()->json([
|
||||||
|
'success' => false,
|
||||||
|
'message' => 'An error occurred while fetching the worker list.',
|
||||||
|
'error' => app()->environment('local') ? $e->getMessage() : 'Internal Server Error'
|
||||||
|
], 500);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 2. GET /api/employers/candidates
|
||||||
|
* List of candidates representing standard job applications and direct hiring offers.
|
||||||
|
*/
|
||||||
|
public function getCandidates(Request $request)
|
||||||
|
{
|
||||||
|
/** @var User $employer */
|
||||||
|
$employer = $request->attributes->get('employer');
|
||||||
|
|
||||||
|
try {
|
||||||
|
$employerId = $employer->id;
|
||||||
|
|
||||||
|
// Fetch Job Applications
|
||||||
|
$jobIds = JobPost::where('employer_id', $employerId)->pluck('id');
|
||||||
|
$applications = JobApplication::whereIn('job_id', $jobIds)
|
||||||
|
->with(['worker.category', 'jobPost'])
|
||||||
|
->get();
|
||||||
|
|
||||||
|
$selectedWorkers = $applications->map(function ($app) {
|
||||||
|
$w = $app->worker;
|
||||||
|
if (!$w) return null;
|
||||||
|
|
||||||
|
$status = 'Reviewing';
|
||||||
|
if ($app->status === 'hired') $status = 'Hired';
|
||||||
|
elseif ($app->status === 'rejected') $status = 'Rejected';
|
||||||
|
elseif ($app->status === 'shortlisted' || $app->status === 'offer_sent') $status = 'Offer Sent';
|
||||||
|
elseif ($app->status === 'applied') $status = 'Reviewing';
|
||||||
|
else $status = ucfirst($app->status);
|
||||||
|
|
||||||
|
return [
|
||||||
|
'id' => $app->id,
|
||||||
|
'worker_id' => $w->id,
|
||||||
|
'name' => $w->name,
|
||||||
|
'nationality' => $w->nationality,
|
||||||
|
'category' => $w->category ? $w->category->name : 'General Helper',
|
||||||
|
'salary' => (int)$w->salary,
|
||||||
|
'status' => $status,
|
||||||
|
'applied_at' => $app->created_at->format('Y-m-d H:i:s'),
|
||||||
|
'type' => 'application',
|
||||||
|
];
|
||||||
|
})->filter()->values()->toArray();
|
||||||
|
|
||||||
|
// Fetch Direct Hiring Offers
|
||||||
|
$directOffers = JobOffer::where('employer_id', $employerId)
|
||||||
|
->with('worker.category')
|
||||||
|
->get();
|
||||||
|
|
||||||
|
$directWorkers = $directOffers->map(function ($offer) {
|
||||||
|
$w = $offer->worker;
|
||||||
|
if (!$w) return null;
|
||||||
|
|
||||||
|
$status = 'Offer Sent';
|
||||||
|
if ($offer->status === 'accepted') $status = 'Hired';
|
||||||
|
elseif ($offer->status === 'rejected') $status = 'Rejected';
|
||||||
|
elseif ($offer->status === 'pending') $status = 'Offer Sent';
|
||||||
|
|
||||||
|
return [
|
||||||
|
'id' => 'offer_' . $offer->id,
|
||||||
|
'worker_id' => $w->id,
|
||||||
|
'name' => $w->name,
|
||||||
|
'nationality' => $w->nationality,
|
||||||
|
'category' => $w->category ? $w->category->name : 'General Helper',
|
||||||
|
'salary' => (int)$offer->salary,
|
||||||
|
'status' => $status,
|
||||||
|
'applied_at' => $offer->created_at->format('Y-m-d H:i:s'),
|
||||||
|
'type' => 'direct_offer',
|
||||||
|
];
|
||||||
|
})->filter()->values()->toArray();
|
||||||
|
|
||||||
|
// Merge and sort
|
||||||
|
$mergedCandidates = array_merge($selectedWorkers, $directWorkers);
|
||||||
|
|
||||||
|
// Optional status filtering
|
||||||
|
if ($request->filled('status')) {
|
||||||
|
$filterStatus = $request->status;
|
||||||
|
$mergedCandidates = array_values(array_filter($mergedCandidates, function ($c) use ($filterStatus) {
|
||||||
|
return strcasecmp($c['status'], $filterStatus) === 0;
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
return response()->json([
|
||||||
|
'success' => true,
|
||||||
|
'data' => [
|
||||||
|
'candidates' => $mergedCandidates
|
||||||
|
]
|
||||||
|
], 200);
|
||||||
|
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
logger()->error('Mobile API Employer Get Candidates Failure: ' . $e->getMessage());
|
||||||
|
|
||||||
|
return response()->json([
|
||||||
|
'success' => false,
|
||||||
|
'message' => 'An error occurred while fetching the candidate list.',
|
||||||
|
'error' => app()->environment('local') ? $e->getMessage() : 'Internal Server Error'
|
||||||
|
], 500);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 3. POST /api/employers/candidates/hire or POST /api/employers/candidates/{id}/hire
|
||||||
|
* Update status from active (or reviewing/pending) to hired.
|
||||||
|
*/
|
||||||
|
public function hireCandidate(Request $request, $id = null)
|
||||||
|
{
|
||||||
|
/** @var User $employer */
|
||||||
|
$employer = $request->attributes->get('employer');
|
||||||
|
|
||||||
|
// Let the client pass either route parameter id OR body parameter identifier
|
||||||
|
$targetId = $id ?: $request->input('id') ?: $request->input('worker_id') ?: $request->input('application_id') ?: $request->input('offer_id');
|
||||||
|
|
||||||
|
if (!$targetId) {
|
||||||
|
return response()->json([
|
||||||
|
'success' => false,
|
||||||
|
'message' => 'Candidate or Worker identifier is required.'
|
||||||
|
], 422);
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
$employerId = $employer->id;
|
||||||
|
$worker = null;
|
||||||
|
$updatedApplication = null;
|
||||||
|
$updatedOffer = 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' => 'accepted']);
|
||||||
|
if ($offer->worker) {
|
||||||
|
$offer->worker->update(['status' => 'Hired']);
|
||||||
|
$worker = $offer->worker;
|
||||||
|
}
|
||||||
|
$updatedOffer = $offer;
|
||||||
|
} else {
|
||||||
|
// Check if targetId is an application
|
||||||
|
$application = JobApplication::where('id', $targetId)
|
||||||
|
->whereHas('jobPost', function($q) use ($employerId) {
|
||||||
|
$q->where('employer_id', $employerId);
|
||||||
|
})->first();
|
||||||
|
|
||||||
|
if ($application) {
|
||||||
|
$application->update(['status' => 'hired']);
|
||||||
|
if ($application->worker) {
|
||||||
|
$application->worker->update(['status' => 'Hired']);
|
||||||
|
$worker = $application->worker;
|
||||||
|
}
|
||||||
|
$updatedApplication = $application;
|
||||||
|
} else {
|
||||||
|
// Try targeting by Worker ID directly
|
||||||
|
$worker = Worker::find($targetId);
|
||||||
|
|
||||||
|
if (!$worker) {
|
||||||
|
return response()->json([
|
||||||
|
'success' => false,
|
||||||
|
'message' => 'Worker, application, or job offer not found with the provided identifier.'
|
||||||
|
], 404);
|
||||||
|
}
|
||||||
|
|
||||||
|
$worker->update(['status' => 'Hired']);
|
||||||
|
|
||||||
|
// Find existing direct offer
|
||||||
|
$offer = JobOffer::where('employer_id', $employerId)
|
||||||
|
->where('worker_id', $worker->id)
|
||||||
|
->first();
|
||||||
|
|
||||||
|
if ($offer) {
|
||||||
|
$offer->update(['status' => 'accepted']);
|
||||||
|
$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();
|
||||||
|
|
||||||
|
if ($application) {
|
||||||
|
$application->update(['status' => 'hired']);
|
||||||
|
$updatedApplication = $application;
|
||||||
|
} else {
|
||||||
|
// Create a new direct job offer and accept it
|
||||||
|
$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 finalized via mobile API.',
|
||||||
|
'status' => 'accepted',
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return response()->json([
|
||||||
|
'success' => true,
|
||||||
|
'message' => 'Candidate successfully marked as Hired!',
|
||||||
|
'data' => [
|
||||||
|
'worker_id' => $worker ? $worker->id : null,
|
||||||
|
'worker_status' => $worker ? $worker->status : 'Hired',
|
||||||
|
'application' => $updatedApplication,
|
||||||
|
'offer' => $updatedOffer,
|
||||||
|
]
|
||||||
|
], 200);
|
||||||
|
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
logger()->error('Mobile API Employer Hire Candidate Failure: ' . $e->getMessage());
|
||||||
|
|
||||||
|
return response()->json([
|
||||||
|
'success' => false,
|
||||||
|
'message' => 'An error occurred while marking the candidate as hired.',
|
||||||
|
'error' => app()->environment('local') ? $e->getMessage() : 'Internal Server Error'
|
||||||
|
], 500);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -103,6 +103,11 @@ public function index(Request $request)
|
|||||||
// Merge standard job applications with direct hiring offers for unified tracking
|
// Merge standard job applications with direct hiring offers for unified tracking
|
||||||
$mergedCandidates = array_merge($selectedWorkers, $directWorkers);
|
$mergedCandidates = array_merge($selectedWorkers, $directWorkers);
|
||||||
|
|
||||||
|
// Only display Hired candidates in the candidates pipeline
|
||||||
|
$mergedCandidates = array_values(array_filter($mergedCandidates, function ($w) {
|
||||||
|
return $w && $w['status'] === 'Hired';
|
||||||
|
}));
|
||||||
|
|
||||||
return Inertia::render('Employer/SelectedCandidates', [
|
return Inertia::render('Employer/SelectedCandidates', [
|
||||||
'selectedWorkers' => $mergedCandidates,
|
'selectedWorkers' => $mergedCandidates,
|
||||||
]);
|
]);
|
||||||
|
|||||||
@ -42,7 +42,10 @@ public function index(Request $request)
|
|||||||
$user = $this->resolveCurrentUser();
|
$user = $this->resolveCurrentUser();
|
||||||
$employerId = $user ? $user->id : 2;
|
$employerId = $user ? $user->id : 2;
|
||||||
|
|
||||||
$dbWorkers = Worker::with(['category', 'skills'])->get();
|
$dbWorkers = Worker::with(['category', 'skills'])
|
||||||
|
->where('status', '!=', 'Hired')
|
||||||
|
->where('status', '!=', 'hidden')
|
||||||
|
->get();
|
||||||
|
|
||||||
$workers = $dbWorkers->map(function ($w) {
|
$workers = $dbWorkers->map(function ($w) {
|
||||||
// Map languages with country names
|
// Map languages with country names
|
||||||
@ -177,6 +180,8 @@ public function show($id)
|
|||||||
|
|
||||||
$simDb = Worker::with('category')
|
$simDb = Worker::with('category')
|
||||||
->where('id', '!=', $w->id)
|
->where('id', '!=', $w->id)
|
||||||
|
->where('status', '!=', 'Hired')
|
||||||
|
->where('status', '!=', 'hidden')
|
||||||
->limit(3)
|
->limit(3)
|
||||||
->get();
|
->get();
|
||||||
$similarWorkers = $simDb->map(function($sw) {
|
$similarWorkers = $simDb->map(function($sw) {
|
||||||
@ -254,4 +259,47 @@ public function sendOffer(Request $request, $id)
|
|||||||
return redirect()->route('employer.hiring.success', ['id' => $id])
|
return redirect()->route('employer.hiring.success', ['id' => $id])
|
||||||
->with('success', 'Hiring offer has been successfully dispatched to the worker!');
|
->with('success', 'Hiring offer has been successfully dispatched to the worker!');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function markHired(Request $request, $id)
|
||||||
|
{
|
||||||
|
$user = $this->resolveCurrentUser();
|
||||||
|
$employerId = $user ? $user->id : 2;
|
||||||
|
|
||||||
|
$worker = Worker::findOrFail($id);
|
||||||
|
$worker->update([
|
||||||
|
'status' => 'Hired',
|
||||||
|
]);
|
||||||
|
|
||||||
|
// 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();
|
||||||
|
|
||||||
|
if ($offer) {
|
||||||
|
$offer->update(['status' => 'accepted']);
|
||||||
|
} 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();
|
||||||
|
|
||||||
|
if ($application) {
|
||||||
|
$application->update(['status' => 'hired']);
|
||||||
|
} else {
|
||||||
|
// Create a new direct job offer with status accepted
|
||||||
|
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 finalized.',
|
||||||
|
'status' => 'accepted',
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return back()->with('success', 'Worker successfully marked as Hired!');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -50,44 +50,6 @@ export default function SelectedCandidates({ selectedWorkers }) {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const stats = [
|
|
||||||
{
|
|
||||||
label: t('total_candidates', 'Total Candidates'),
|
|
||||||
count: workers.length,
|
|
||||||
icon: Users,
|
|
||||||
color: 'text-slate-600',
|
|
||||||
bg: 'bg-slate-50'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: t('reviewing', 'Reviewing'),
|
|
||||||
count: workers.filter(w => w.status === 'Reviewing' || !w.status).length,
|
|
||||||
icon: Search,
|
|
||||||
color: 'text-amber-600',
|
|
||||||
bg: 'bg-amber-50'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: t('offer_sent', 'Offer Sent'),
|
|
||||||
count: workers.filter(w => w.status === 'Offer Sent').length,
|
|
||||||
icon: Send,
|
|
||||||
color: 'text-blue-600',
|
|
||||||
bg: 'bg-blue-50'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: t('hired', 'Hired'),
|
|
||||||
count: workers.filter(w => w.status === 'Hired').length,
|
|
||||||
icon: CheckCircle2,
|
|
||||||
color: 'text-emerald-600',
|
|
||||||
bg: 'bg-emerald-50'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: t('rejected', 'Rejected'),
|
|
||||||
count: workers.filter(w => w.status === 'Rejected').length,
|
|
||||||
icon: UserCheck,
|
|
||||||
color: 'text-rose-600',
|
|
||||||
bg: 'bg-rose-50'
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<EmployerLayout title={t('candidates_pipeline', 'Candidates')}>
|
<EmployerLayout title={t('candidates_pipeline', 'Candidates')}>
|
||||||
<Head title={`${t('candidates_pipeline', 'Candidates')} - ${t('employer_portal', 'Employer Portal')}`} />
|
<Head title={`${t('candidates_pipeline', 'Candidates')} - ${t('employer_portal', 'Employer Portal')}`} />
|
||||||
@ -101,21 +63,6 @@ export default function SelectedCandidates({ selectedWorkers }) {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Stats Grid */}
|
|
||||||
<div className="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 lg:grid-cols-5 gap-4">
|
|
||||||
{stats.map((stat) => (
|
|
||||||
<div key={stat.label} className="bg-white p-5 rounded-2xl border border-slate-100 shadow-sm flex items-center space-x-4">
|
|
||||||
<div className={`p-3 rounded-xl ${stat.bg}`}>
|
|
||||||
<stat.icon className={`w-6 h-6 ${stat.color}`} />
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<div className="text-2xl font-black text-slate-900 leading-none">{stat.count}</div>
|
|
||||||
<div className="text-[10px] font-black text-slate-400 uppercase tracking-widest mt-1">{stat.label}</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Table Section */}
|
{/* Table Section */}
|
||||||
<div className="bg-white rounded-3xl border border-slate-200 shadow-sm overflow-hidden">
|
<div className="bg-white rounded-3xl border border-slate-200 shadow-sm overflow-hidden">
|
||||||
<div className="p-6 border-b border-slate-100 bg-slate-50/50 flex items-center justify-between">
|
<div className="p-6 border-b border-slate-100 bg-slate-50/50 flex items-center justify-between">
|
||||||
@ -179,45 +126,10 @@ export default function SelectedCandidates({ selectedWorkers }) {
|
|||||||
<span className="text-sm font-bold text-slate-800">{worker.salary} <span className="text-[10px] text-slate-400">AED</span></span>
|
<span className="text-sm font-bold text-slate-800">{worker.salary} <span className="text-[10px] text-slate-400">AED</span></span>
|
||||||
</td>
|
</td>
|
||||||
<td className="px-6 py-5">
|
<td className="px-6 py-5">
|
||||||
<DropdownMenu>
|
<span className="inline-flex items-center px-2.5 py-1 rounded-full text-[10px] font-black tracking-tight bg-emerald-50 text-emerald-700 border border-emerald-100 shadow-xs">
|
||||||
<DropdownMenuTrigger asChild>
|
<span className="w-1 h-1 rounded-full mr-1.5 bg-emerald-500" />
|
||||||
<button className={`inline-flex items-center px-2.5 py-1 rounded-full text-[10px] font-black tracking-tight hover:ring-2 hover:ring-offset-1 transition-all ${
|
{t('hired', 'Hired').toUpperCase()}
|
||||||
worker.status === 'Hired' ? 'bg-emerald-50 text-emerald-700 hover:ring-emerald-200' :
|
</span>
|
||||||
worker.status === 'Rejected' ? 'bg-rose-50 text-rose-700 hover:ring-rose-200' :
|
|
||||||
worker.status === 'Offer Sent' ? 'bg-blue-50 text-blue-700 hover:ring-blue-200' :
|
|
||||||
'bg-amber-50 text-amber-700 hover:ring-amber-200'
|
|
||||||
}`}>
|
|
||||||
<span className={`w-1 h-1 rounded-full mr-1.5 ${
|
|
||||||
worker.status === 'Hired' ? 'bg-emerald-500' :
|
|
||||||
worker.status === 'Rejected' ? 'bg-rose-500' :
|
|
||||||
worker.status === 'Offer Sent' ? 'bg-blue-500' :
|
|
||||||
'bg-amber-500'
|
|
||||||
}`} />
|
|
||||||
{worker.status === 'Hired' ? t('hired', 'Hired').toUpperCase() :
|
|
||||||
worker.status === 'Rejected' ? t('rejected', 'Rejected').toUpperCase() :
|
|
||||||
worker.status === 'Offer Sent' ? t('offer_sent', 'Offer Sent').toUpperCase() :
|
|
||||||
t('reviewing', 'Reviewing').toUpperCase()}
|
|
||||||
<ChevronRight className="w-3 h-3 ml-1 rotate-90 opacity-50" />
|
|
||||||
</button>
|
|
||||||
</DropdownMenuTrigger>
|
|
||||||
<DropdownMenuContent align="start" className="w-44 p-2 rounded-xl">
|
|
||||||
<DropdownMenuLabel className="text-[10px] font-black text-slate-400 uppercase tracking-widest px-2 py-1.5">{t('change_status_label', 'Change Status')}</DropdownMenuLabel>
|
|
||||||
<DropdownMenuSeparator />
|
|
||||||
<DropdownMenuItem onClick={() => changeStatus(worker.id, 'Reviewing')} className="text-xs font-bold text-slate-600 focus:text-amber-700 focus:bg-amber-50 rounded-lg cursor-pointer">
|
|
||||||
{t('reviewing', 'Reviewing')}
|
|
||||||
</DropdownMenuItem>
|
|
||||||
<DropdownMenuItem onClick={() => changeStatus(worker.id, 'Offer Sent')} className="text-xs font-bold text-slate-600 focus:text-blue-700 focus:bg-blue-50 rounded-lg cursor-pointer">
|
|
||||||
{t('offer_sent', 'Offer Sent')}
|
|
||||||
</DropdownMenuItem>
|
|
||||||
<DropdownMenuItem onClick={() => changeStatus(worker.id, 'Hired')} className="text-xs font-bold text-slate-600 focus:text-emerald-700 focus:bg-emerald-50 rounded-lg cursor-pointer">
|
|
||||||
{t('hired', 'Hired')}
|
|
||||||
</DropdownMenuItem>
|
|
||||||
<DropdownMenuSeparator />
|
|
||||||
<DropdownMenuItem onClick={() => changeStatus(worker.id, 'Rejected')} className="text-xs font-bold text-rose-600 focus:text-rose-700 focus:bg-rose-50 rounded-lg cursor-pointer">
|
|
||||||
{t('rejected', 'Rejected')}
|
|
||||||
</DropdownMenuItem>
|
|
||||||
</DropdownMenuContent>
|
|
||||||
</DropdownMenu>
|
|
||||||
</td>
|
</td>
|
||||||
<td className="px-6 py-5 text-right">
|
<td className="px-6 py-5 text-right">
|
||||||
<div className="flex items-center justify-end space-x-1">
|
<div className="flex items-center justify-end space-x-1">
|
||||||
|
|||||||
@ -113,11 +113,16 @@ export default function Show({ worker }) {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const handleMarkHired = () => {
|
const handleMarkHired = () => {
|
||||||
|
router.post(`/employer/workers/${worker.id}/mark-hired`, {}, {
|
||||||
|
preserveScroll: true,
|
||||||
|
onSuccess: () => {
|
||||||
setAvailabilityStatus('Hired');
|
setAvailabilityStatus('Hired');
|
||||||
setShowHiredModal(true);
|
setShowHiredModal(true);
|
||||||
toast.success(t('marked_hired', 'Worker successfully marked as Hired!'), {
|
toast.success(t('marked_hired', 'Worker successfully marked as Hired!'), {
|
||||||
description: t('marked_hired_desc', 'Sponsorship direct match finalized. Please leave an anonymous trust review!')
|
description: t('marked_hired_desc', 'Sponsorship direct match finalized. Please leave an anonymous trust review!')
|
||||||
});
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@ -256,6 +261,7 @@ export default function Show({ worker }) {
|
|||||||
</Link>
|
</Link>
|
||||||
|
|
||||||
{/* Stage 4 Hire: Mark Hired (Finalize Hiring Direct Flow) */}
|
{/* Stage 4 Hire: Mark Hired (Finalize Hiring Direct Flow) */}
|
||||||
|
{availabilityStatus !== 'Hired' && (
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={handleMarkHired}
|
onClick={handleMarkHired}
|
||||||
@ -264,6 +270,7 @@ export default function Show({ worker }) {
|
|||||||
<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('mark_hired_action', 'Mark Hired')}</span>
|
||||||
</button>
|
</button>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@ -81,4 +81,10 @@
|
|||||||
Route::get('/employers/announcements', [EmployerAnnouncementController::class, 'getAnnouncements']);
|
Route::get('/employers/announcements', [EmployerAnnouncementController::class, 'getAnnouncements']);
|
||||||
Route::post('/employers/announcements', [EmployerAnnouncementController::class, 'createAnnouncement']);
|
Route::post('/employers/announcements', [EmployerAnnouncementController::class, 'createAnnouncement']);
|
||||||
Route::delete('/employers/announcements/{id}', [EmployerAnnouncementController::class, 'deleteAnnouncement']);
|
Route::delete('/employers/announcements/{id}', [EmployerAnnouncementController::class, 'deleteAnnouncement']);
|
||||||
|
|
||||||
|
// Worker and Candidate Pipeline Management
|
||||||
|
Route::get('/employers/workers', [\App\Http\Controllers\Api\EmployerWorkerController::class, 'getWorkers']);
|
||||||
|
Route::get('/employers/candidates', [\App\Http\Controllers\Api\EmployerWorkerController::class, 'getCandidates']);
|
||||||
|
Route::post('/employers/candidates/{id}/hire', [\App\Http\Controllers\Api\EmployerWorkerController::class, 'hireCandidate']);
|
||||||
|
Route::post('/employers/candidates/hire', [\App\Http\Controllers\Api\EmployerWorkerController::class, 'hireCandidate']);
|
||||||
});
|
});
|
||||||
|
|||||||
@ -110,6 +110,7 @@
|
|||||||
return Inertia::render('Employer/Hiring/Confirm', ['worker' => $workerData]);
|
return Inertia::render('Employer/Hiring/Confirm', ['worker' => $workerData]);
|
||||||
})->name('employer.hiring.confirm');
|
})->name('employer.hiring.confirm');
|
||||||
Route::post('/workers/{id}/hire', [\App\Http\Controllers\Employer\WorkerController::class, 'sendOffer'])->name('employer.workers.send-offer');
|
Route::post('/workers/{id}/hire', [\App\Http\Controllers\Employer\WorkerController::class, 'sendOffer'])->name('employer.workers.send-offer');
|
||||||
|
Route::post('/workers/{id}/mark-hired', [\App\Http\Controllers\Employer\WorkerController::class, 'markHired'])->name('employer.workers.mark-hired');
|
||||||
Route::get('/workers/{id}/hire/success', function ($id) {
|
Route::get('/workers/{id}/hire/success', function ($id) {
|
||||||
$worker = \App\Models\Worker::findOrFail($id);
|
$worker = \App\Models\Worker::findOrFail($id);
|
||||||
$workerData = [
|
$workerData = [
|
||||||
|
|||||||
@ -1,5 +1,6 @@
|
|||||||
import { defineConfig } from 'vite';
|
import { defineConfig } from 'vite';
|
||||||
import laravel from 'laravel-vite-plugin';
|
import laravel from 'laravel-vite-plugin';
|
||||||
|
import react from '@vitejs/plugin-react';
|
||||||
import { bunny } from 'laravel-vite-plugin/fonts';
|
import { bunny } from 'laravel-vite-plugin/fonts';
|
||||||
import tailwindcss from '@tailwindcss/vite';
|
import tailwindcss from '@tailwindcss/vite';
|
||||||
|
|
||||||
@ -14,9 +15,11 @@ export default defineConfig({
|
|||||||
}),
|
}),
|
||||||
],
|
],
|
||||||
}),
|
}),
|
||||||
|
react(),
|
||||||
tailwindcss(),
|
tailwindcss(),
|
||||||
],
|
],
|
||||||
server: {
|
server: {
|
||||||
|
host: 'localhost',
|
||||||
watch: {
|
watch: {
|
||||||
ignored: ['**/storage/framework/views/**'],
|
ignored: ['**/storage/framework/views/**'],
|
||||||
},
|
},
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user