mohan #25

Merged
mohanmd merged 10 commits from mohan into master 2026-07-04 16:10:27 +00:00
13 changed files with 669 additions and 303 deletions
Showing only changes of commit 51193f3188 - Show all commits

View File

@ -57,6 +57,7 @@ private function formatWorker(Worker $w)
'deleted_at' => $w->deleted_at?->toISOString(),
'language' => $w->language,
'languages' => $langs,
'main_profession' => $w->main_profession,
'preferred_location' => $w->preferred_location,
'country' => $w->country,
'city' => $w->city,
@ -154,6 +155,13 @@ public function getWorkers(Request $request)
$workersArray = $formattedWorkers->toArray();
// Apply filters: gender, skills, language/languages, nationality, preferred_location, job_type, live_in_out, in_country, visa_status
if ($request->filled('main_profession')) {
$mainProfession = strtolower($request->main_profession);
$workersArray = array_values(array_filter($workersArray, function ($c) use ($mainProfession) {
return isset($c['main_profession']) && strtolower($c['main_profession']) === $mainProfession;
}));
}
if ($request->filled('gender')) {
$gender = strtolower($request->gender);
$workersArray = array_values(array_filter($workersArray, function ($c) use ($gender) {
@ -481,6 +489,13 @@ public function getCandidates(Request $request)
}));
// Apply filters: gender, skills, languages, nationality, availability, preferred_location, job_type, live_in_out, in_country, visa_status
if ($request->filled('main_profession')) {
$mainProfession = strtolower($request->main_profession);
$mergedCandidates = array_values(array_filter($mergedCandidates, function ($c) use ($mainProfession) {
return isset($c['main_profession']) && strtolower($c['main_profession']) === $mainProfession;
}));
}
if ($request->filled('gender')) {
$gender = strtolower($request->gender);
$mergedCandidates = array_values(array_filter($mergedCandidates, function ($c) use ($gender) {

View File

@ -181,6 +181,7 @@ public function setupProfile(Request $request)
'gender' => 'nullable|string|in:male,female,other',
'live_in_out' => 'nullable|string|in:live_in,live_out',
'preferred_location' => 'nullable|string|max:255',
'main_profession' => 'nullable|string|max:100',
'skills' => 'nullable|array',
'skills.*' => 'integer|exists:skills,id',
'fcm_token' => 'nullable|string|max:255',
@ -241,6 +242,7 @@ public function setupProfile(Request $request)
'gender' => $request->gender,
'live_in_out' => $request->live_in_out,
'preferred_location' => $request->preferred_location,
'main_profession' => $request->main_profession,
'age' => 25, // Default — updated later in full profile
'salary' => 1500, // Default AED — updated later
'availability' => 'Immediate',
@ -318,6 +320,7 @@ public function register(Request $request)
'in_country' => 'nullable',
'visa_status' => 'nullable|string|max:100',
'preferred_job_type' => 'nullable|string|max:100',
'main_profession' => 'nullable|string|max:100',
'gender' => 'nullable|string|in:male,female,other',
'live_in_out' => 'nullable|string|in:live_in,live_out',
'preferred_location' => 'nullable|string|max:255',
@ -453,6 +456,7 @@ public function register(Request $request)
'gender' => $request->gender,
'live_in_out' => $request->live_in_out,
'preferred_location' => $request->preferred_location,
'main_profession' => $request->main_profession,
'age' => $age,
'salary' => $request->salary,
'availability' => 'Immediate',

View File

@ -91,6 +91,7 @@ public function index(Request $request)
'emirates_id_status' => $emiratesIdStatus,
'passport_status' => $w->passport_status,
'category' => 'Domestic Worker',
'main_profession' => $w->main_profession,
'skills' => $mappedSkills,
'visa_status' => $visaStatus,
'experience' => $w->experience,
@ -192,6 +193,12 @@ public function index(Request $request)
return false;
}));
}
if ($request->filled('main_profession')) {
$mainProfession = strtolower($request->main_profession);
$workers = array_values(array_filter($workers, function ($c) use ($mainProfession) {
return isset($c['main_profession']) && strtolower($c['main_profession']) === $mainProfession;
}));
}
if ($request->filled('gender')) {
$gender = strtolower($request->gender);
$workers = array_values(array_filter($workers, function ($c) use ($gender) {
@ -268,6 +275,7 @@ public function index(Request $request)
$filtersMetadata = [
'nationalities' => array_merge(['All Nationalities'], $dbNationalities),
'professions' => ['All Professions', 'Housemaid', 'Nanny', 'Cook', 'Driver', 'Caregiver'],
'experienceLevels' => ['All Experience', '1-2 Years', '3-5 Years', '5+ Years'],
'religions' => ['All Religions', 'Christian', 'Muslim', 'Hindu', 'Buddhist'],
'languages' => ['All Languages', 'English', 'Arabic', 'Hindi', 'Tagalog'],
@ -367,6 +375,7 @@ public function show($id)
'emirates_id_status' => $emiratesIdStatus,
'passport_status' => $w->passport_status,
'category' => 'Domestic Worker',
'main_profession' => $w->main_profession,
'skills' => $mappedSkills,
'visa_status' => $visaStatus,
'experience' => $w->experience,

View File

@ -35,6 +35,7 @@ class Worker extends Model
'in_country',
'visa_status',
'preferred_job_type',
'main_profession',
'fcm_token',
];

View File

@ -0,0 +1,28 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::table('workers', function (Blueprint $table) {
$table->string('main_profession')->nullable()->after('preferred_job_type');
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::table('workers', function (Blueprint $table) {
$table->dropColumn('main_profession');
});
}
};

View File

@ -63,28 +63,79 @@
"type": "object",
"description": "Optional JSON object containing extracted organization/trade license details.",
"properties": {
"document_type": { "type": "string", "example": "dubai_commercial_license" },
"country": { "type": "string", "example": "United Arab Emirates" },
"authority": { "type": "string", "example": "Dubai Economy and Tourism" },
"license_no": { "type": "string", "example": "1426961" },
"company_name": { "type": "string", "example": "NEST CLIMATE TECHNICAL SERVICES L.L.C" },
"business_name": { "type": "string", "example": "NEST CLIMATE TECHNICAL SERVICES L.L.C" },
"license_category": { "type": "string", "example": "Dep. of Economic Development" },
"legal_type": { "type": "string", "example": "Limited Liability Company(LLC)" },
"issue_date": { "type": "string", "example": "22/10/2024" },
"expiry_date": { "type": "string", "example": "21/10/2026" },
"main_license_no": { "type": "string", "example": "1426961" },
"register_no": { "type": "string", "example": "2436760" },
"dcci_no": { "type": "string", "example": "570232" },
"document_type": {
"type": "string",
"example": "dubai_commercial_license"
},
"country": {
"type": "string",
"example": "United Arab Emirates"
},
"authority": {
"type": "string",
"example": "Dubai Economy and Tourism"
},
"license_no": {
"type": "string",
"example": "1426961"
},
"company_name": {
"type": "string",
"example": "NEST CLIMATE TECHNICAL SERVICES L.L.C"
},
"business_name": {
"type": "string",
"example": "NEST CLIMATE TECHNICAL SERVICES L.L.C"
},
"license_category": {
"type": "string",
"example": "Dep. of Economic Development"
},
"legal_type": {
"type": "string",
"example": "Limited Liability Company(LLC)"
},
"issue_date": {
"type": "string",
"example": "22/10/2024"
},
"expiry_date": {
"type": "string",
"example": "21/10/2026"
},
"main_license_no": {
"type": "string",
"example": "1426961"
},
"register_no": {
"type": "string",
"example": "2436760"
},
"dcci_no": {
"type": "string",
"example": "570232"
},
"members": {
"type": "array",
"items": {
"type": "object",
"properties": {
"person_no": { "type": "string", "example": "1709285" },
"name": { "type": "string", "example": "SEYED ABDUL RAHMAN SHERIF NISAR" },
"nationality": { "type": "string", "example": "India" },
"role": { "type": "string", "example": "Manager" }
"person_no": {
"type": "string",
"example": "1709285"
},
"name": {
"type": "string",
"example": "SEYED ABDUL RAHMAN SHERIF NISAR"
},
"nationality": {
"type": "string",
"example": "India"
},
"role": {
"type": "string",
"example": "Manager"
}
}
}
}
@ -138,7 +189,6 @@
"type": "string",
"example": "151023946"
}
}
},
"organization_name": {
@ -976,6 +1026,18 @@
"example": "full-time",
"description": "Preferred job type."
},
"main_profession": {
"type": "string",
"example": "Housemaid",
"description": "Main profession of the worker selected from the master dropdown.",
"enum": [
"Housemaid",
"Nanny",
"Cook",
"Driver",
"Caregiver"
]
},
"live_in_out": {
"type": "string",
"enum": [
@ -2333,6 +2395,18 @@
"example": "Dubai Marina",
"description": "Preferred work location/area."
},
"main_profession": {
"type": "string",
"example": "Nanny",
"description": "Main profession of the worker selected from the master dropdown.",
"enum": [
"Housemaid",
"Nanny",
"Cook",
"Driver",
"Caregiver"
]
},
"fcm_token": {
"type": "string",
"example": "fcm_token_example",
@ -5054,6 +5128,15 @@
"type": "string"
}
},
{
"name": "main_profession",
"in": "query",
"required": false,
"description": "Filter by main profession (e.g. Housemaid, Nanny, Cook, Driver, Caregiver).",
"schema": {
"type": "string"
}
},
{
"name": "preferred_location",
"in": "query",
@ -6245,14 +6328,44 @@
"description"
],
"properties": {
"title": { "type": "string", "example": "Housekeeper Needed" },
"workers_needed": { "type": "integer", "example": 2 },
"location": { "type": "string", "example": "Dubai Marina" },
"salary": { "type": "number", "example": 1800 },
"job_type": { "type": "string", "enum": ["Full Time", "Part Time", "Contract"], "example": "Full Time" },
"start_date": { "type": "string", "format": "date", "example": "2026-07-15" },
"description": { "type": "string", "example": "Looking for an experienced housekeeper for a family in Dubai Marina." },
"requirements": { "type": "string", "example": "Must speak fluent English. Minimum 3 years experience." }
"title": {
"type": "string",
"example": "Housekeeper Needed"
},
"workers_needed": {
"type": "integer",
"example": 2
},
"location": {
"type": "string",
"example": "Dubai Marina"
},
"salary": {
"type": "number",
"example": 1800
},
"job_type": {
"type": "string",
"enum": [
"Full Time",
"Part Time",
"Contract"
],
"example": "Full Time"
},
"start_date": {
"type": "string",
"format": "date",
"example": "2026-07-15"
},
"description": {
"type": "string",
"example": "Looking for an experienced housekeeper for a family in Dubai Marina."
},
"requirements": {
"type": "string",
"example": "Must speak fluent English. Minimum 3 years experience."
}
}
}
}
@ -6288,7 +6401,9 @@
"name": "id",
"in": "path",
"required": true,
"schema": { "type": "integer" }
"schema": {
"type": "integer"
}
}
],
"responses": {
@ -6319,7 +6434,9 @@
"name": "id",
"in": "path",
"required": true,
"schema": { "type": "integer" }
"schema": {
"type": "integer"
}
}
],
"responses": {
@ -6352,7 +6469,9 @@
"name": "id",
"in": "path",
"required": true,
"schema": { "type": "integer" }
"schema": {
"type": "integer"
}
}
],
"requestBody": {
@ -6372,15 +6491,53 @@
"status"
],
"properties": {
"title": { "type": "string", "example": "Housekeeper Needed" },
"workers_needed": { "type": "integer", "example": 2 },
"location": { "type": "string", "example": "Dubai Marina" },
"salary": { "type": "number", "example": 1800 },
"job_type": { "type": "string", "enum": ["Full Time", "Part Time", "Contract"], "example": "Full Time" },
"start_date": { "type": "string", "format": "date", "example": "2026-07-15" },
"description": { "type": "string", "example": "Looking for an experienced housekeeper for a family in Dubai Marina." },
"requirements": { "type": "string", "example": "Must speak fluent English. Minimum 3 years experience." },
"status": { "type": "string", "enum": ["active", "closed", "draft"], "example": "active" }
"title": {
"type": "string",
"example": "Housekeeper Needed"
},
"workers_needed": {
"type": "integer",
"example": 2
},
"location": {
"type": "string",
"example": "Dubai Marina"
},
"salary": {
"type": "number",
"example": 1800
},
"job_type": {
"type": "string",
"enum": [
"Full Time",
"Part Time",
"Contract"
],
"example": "Full Time"
},
"start_date": {
"type": "string",
"format": "date",
"example": "2026-07-15"
},
"description": {
"type": "string",
"example": "Looking for an experienced housekeeper for a family in Dubai Marina."
},
"requirements": {
"type": "string",
"example": "Must speak fluent English. Minimum 3 years experience."
},
"status": {
"type": "string",
"enum": [
"active",
"closed",
"draft"
],
"example": "active"
}
}
}
}
@ -7480,23 +7637,39 @@
"schema": {
"type": "object",
"properties": {
"success": { "type": "boolean", "example": true },
"success": {
"type": "boolean",
"example": true
},
"data": {
"type": "object",
"properties": {
"application": {
"type": "object",
"properties": {
"id": { "type": "integer", "example": 12 },
"status": { "type": "string", "example": "shortlisted" },
"id": {
"type": "integer",
"example": 12
},
"status": {
"type": "string",
"example": "shortlisted"
},
"status_history": {
"type": "array",
"items": {
"type": "object",
"properties": {
"status": { "type": "string" },
"timestamp": { "type": "string" },
"notes": { "type": "string", "nullable": true }
"status": {
"type": "string"
},
"timestamp": {
"type": "string"
},
"notes": {
"type": "string",
"nullable": true
}
}
}
}
@ -7625,34 +7798,103 @@
"license": {
"type": "object",
"properties": {
"document_type": { "type": "string", "example": "dubai_commercial_license" },
"country": { "type": "string", "example": "United Arab Emirates" },
"authority": { "type": "string", "example": "Dubai Economy and Tourism" },
"license_no": { "type": "string", "example": "1426961" },
"company_name": { "type": "string", "example": "NEST CLIMATE TECHNICAL SERVICES L.L.C" },
"business_name": { "type": "string", "example": "NEST CLIMATE TECHNICAL SERVICES L.L.C" },
"license_category": { "type": "string", "example": "Dep. of Economic Development" },
"legal_type": { "type": "string", "example": "Limited Liability Company(LLC)" },
"issue_date": { "type": "string", "example": "22/10/2024" },
"expiry_date": { "type": "string", "example": "21/10/2026" },
"main_license_no": { "type": "string", "example": "1426961" },
"register_no": { "type": "string", "example": "2436760" },
"dcci_no": { "type": "string", "example": "570232" }
"document_type": {
"type": "string",
"example": "dubai_commercial_license"
},
"country": {
"type": "string",
"example": "United Arab Emirates"
},
"authority": {
"type": "string",
"example": "Dubai Economy and Tourism"
},
"license_no": {
"type": "string",
"example": "1426961"
},
"company_name": {
"type": "string",
"example": "NEST CLIMATE TECHNICAL SERVICES L.L.C"
},
"business_name": {
"type": "string",
"example": "NEST CLIMATE TECHNICAL SERVICES L.L.C"
},
"license_category": {
"type": "string",
"example": "Dep. of Economic Development"
},
"legal_type": {
"type": "string",
"example": "Limited Liability Company(LLC)"
},
"issue_date": {
"type": "string",
"example": "22/10/2024"
},
"expiry_date": {
"type": "string",
"example": "21/10/2026"
},
"main_license_no": {
"type": "string",
"example": "1426961"
},
"register_no": {
"type": "string",
"example": "2436760"
},
"dcci_no": {
"type": "string",
"example": "570232"
}
}
},
"emirates_id": {
"type": "object",
"properties": {
"emirates_id_number": { "type": "string", "example": "784-1988-5310327-2" },
"name": { "type": "string", "example": "KRISHNA PRASAD" },
"date_of_birth": { "type": "string", "example": "1988-03-22" },
"issue_date": { "type": "string", "example": "2023-04-11" },
"expiry_date": { "type": "string", "example": "2028-04-11" },
"employer": { "type": "string", "example": "Federal Authority" },
"issue_place": { "type": "string", "example": "Abu Dhabi" },
"occupation": { "type": "string", "example": "Manager" },
"nationality": { "type": "string", "example": "NPL" },
"gender": { "type": "string", "example": "Male" }
"emirates_id_number": {
"type": "string",
"example": "784-1988-5310327-2"
},
"name": {
"type": "string",
"example": "KRISHNA PRASAD"
},
"date_of_birth": {
"type": "string",
"example": "1988-03-22"
},
"issue_date": {
"type": "string",
"example": "2023-04-11"
},
"expiry_date": {
"type": "string",
"example": "2028-04-11"
},
"employer": {
"type": "string",
"example": "Federal Authority"
},
"issue_place": {
"type": "string",
"example": "Abu Dhabi"
},
"occupation": {
"type": "string",
"example": "Manager"
},
"nationality": {
"type": "string",
"example": "NPL"
},
"gender": {
"type": "string",
"example": "Male"
}
}
},
"created_at": {
@ -7834,6 +8076,12 @@
}
}
},
"main_profession": {
"type": "string",
"nullable": true,
"example": "Nanny",
"description": "The main profession of the worker"
},
"skills": {
"type": "array",
"items": {

View File

@ -373,9 +373,9 @@ export default function EmployerLayout({ children, title, fullPage = false }) {
<DropdownMenu>
<DropdownMenuTrigger asChild>
<button className="flex items-center space-x-3 pl-6 border-l border-slate-200 group outline-none">
<div className="text-right hidden sm:block">
<div className="text-xs font-black text-slate-900 group-hover:text-[#185FA5] transition-colors">{user.name}</div>
<div className="text-[10px] font-bold text-slate-400 uppercase tracking-tighter">{t('employer_account', 'Employer Account')}</div>
<div className="text-right hidden sm:block max-w-[180px]">
<div className="text-[11px] font-black text-slate-900 group-hover:text-[#185FA5] transition-colors truncate">{user.name}</div>
<div className="text-[9px] font-bold text-slate-400 uppercase tracking-tighter">{t('employer_account', 'Employer Account')}</div>
</div>
<div className="w-10 h-10 rounded-xl bg-blue-100 text-[#185FA5] flex items-center justify-center font-black text-xs border border-blue-200 shadow-sm transition-transform group-hover:scale-105">
{user.name.charAt(0)}

View File

@ -103,8 +103,8 @@ export default function Dashboard({
<span>{stats.days_remaining} {t('days_left', 'Days Left')}</span>
</div>
<h1 className="text-3xl sm:text-4xl font-black tracking-tight">
{t('welcome_back', 'Welcome Back')}, {employer.name}
<h1 className="text-lg sm:text-xl md:text-2xl font-bold tracking-tight leading-tight text-blue-200">
{t('welcome_back', 'Welcome Back')}, <span className="text-white font-medium">{employer.name}</span>
</h1>
<p className="text-blue-100 text-sm leading-relaxed font-medium">
@ -339,7 +339,14 @@ export default function Dashboard({
<span>{worker.name}</span>
</div>
<div className="text-[10px] text-slate-500 truncate font-semibold">{worker.nationality}</div>
<div className="text-[10px] text-slate-500 truncate font-semibold flex items-center gap-1.5 flex-wrap">
<span>{worker.nationality}</span>
{worker.main_profession && (
<span className="text-[8px] font-black uppercase text-[#185FA5] bg-blue-50/80 px-1 py-0.2 rounded border border-blue-100/50">
{worker.main_profession}
</span>
)}
</div>
</div>
</div>

View File

@ -99,122 +99,120 @@ export default function Shortlist({ shortlistedWorkers }) {
<div key={worker.id} className="bg-white rounded-2xl border border-slate-200 shadow-sm hover:shadow-md transition-all flex flex-col justify-between overflow-hidden group relative">
{/* Card Header with Photo Option and Availability indicator */}
<div className="bg-slate-50/50 p-6 border-b border-slate-100 flex items-start justify-between relative">
<div className="flex items-center space-x-3.5 min-w-0 flex-1">
<div className="bg-slate-50/40 p-4 sm:p-5 pb-3 border-b border-slate-100 flex items-start justify-between relative">
<div className="flex items-center space-x-3 min-w-0 flex-1">
{/* Photo (optional) container */}
<div className="w-14 h-14 rounded-2xl bg-blue-100 text-[#185FA5] flex items-center justify-center font-bold text-xl border border-blue-200 shadow-inner flex-shrink-0 overflow-hidden relative">
<div className="w-12 h-12 rounded-xl bg-blue-50 text-[#185FA5] flex items-center justify-center font-bold text-lg border border-blue-100 shadow-inner flex-shrink-0 overflow-hidden relative">
{worker.photo ? (
<img src={worker.photo} alt={worker.name} className="w-full h-full object-cover" />
) : (
<User className="w-6 h-6 text-slate-400" />
<User className="w-5 h-5 text-slate-400" />
)}
</div>
<div className="min-w-0 flex-1">
<div className="font-extrabold text-base text-slate-900 group-hover:text-[#185FA5] transition-colors truncate flex items-center space-x-1.5">
<div className="min-w-0 flex-1 space-y-1">
<div className="font-extrabold text-sm sm:text-base text-slate-900 group-hover:text-[#185FA5] transition-colors truncate flex items-center gap-1.5 flex-wrap">
<span>{worker.name}</span>
{false && (
<span className="inline-flex items-center px-1.5 py-0.5 rounded-full text-[8px] font-black uppercase tracking-wider bg-emerald-500 text-white shadow-xs gap-0.5" title="OCR Verified">
<CheckCircle2 className="w-3 h-3" />
<span>{t('verified', 'Verified')}</span>
{worker.main_profession && (
<span className="inline-flex items-center px-1.5 py-0.5 rounded text-[8px] font-black uppercase tracking-wider bg-blue-50 text-[#185FA5] border border-blue-100">
{worker.main_profession}
</span>
)}
</div>
{/* Passport Status Verification Badge & Visa Status */}
<div className="flex flex-wrap gap-1 mt-0.5">
{/* Removed visa expiry warning badge for cleaner UI */}
<div className="flex flex-wrap gap-1">
{worker.visa_status && (worker.visa_status.toLowerCase().includes('tourist') || worker.visa_status.toLowerCase().includes('visit')) ? (
<div className="flex items-center space-x-1 text-[9px] font-black uppercase text-amber-700 bg-amber-50 px-1.5 py-0.5 rounded border border-amber-200">
<AlertTriangle className="w-3 h-3 text-amber-600 flex-shrink-0 animate-bounce" />
<span>{worker.visa_status} ({t('warning', 'Warning')})</span>
<div className="flex items-center space-x-0.5 text-[8px] font-black uppercase text-amber-700 bg-amber-50 px-1.5 py-0.5 rounded border border-amber-200">
<AlertTriangle className="w-2.5 h-2.5 text-amber-600 flex-shrink-0 animate-bounce" />
<span>{worker.visa_status}</span>
</div>
) : worker.visa_status && worker.visa_status.toLowerCase().includes('cancelled') ? (
<div className="flex items-center space-x-1 text-[9px] font-black uppercase text-rose-700 bg-rose-50 px-1.5 py-0.5 rounded border border-rose-200 animate-pulse">
<AlertTriangle className="w-3 h-3 text-rose-600 flex-shrink-0" />
<span>{worker.visa_status} ({t('warning', 'Warning')})</span>
<div className="flex items-center space-x-0.5 text-[8px] font-black uppercase text-rose-700 bg-rose-50 px-1.5 py-0.5 rounded border border-rose-200 animate-pulse">
<AlertTriangle className="w-2.5 h-2.5 text-rose-600 flex-shrink-0" />
<span>{worker.visa_status}</span>
</div>
) : (
<div className="flex items-center space-x-1 text-[9px] font-black uppercase text-blue-700 bg-blue-50 px-1.5 py-0.5 rounded border border-blue-100">
<div className="flex items-center space-x-0.5 text-[8px] font-black uppercase text-blue-700 bg-blue-50 px-1.5 py-0.5 rounded border border-blue-100">
<span>{worker.visa_status}</span>
</div>
)}
</div>
<div className="flex items-center space-x-1.5 text-xs text-slate-500 mt-1">
<Globe2 className="w-3.5 h-3.5 text-slate-400" />
<span className="font-bold">{worker.nationality}</span>
<div className="flex items-center space-x-1 text-[10px] text-slate-500 font-bold">
<Globe2 className="w-3 h-3 text-slate-400" />
<span>{worker.nationality}</span>
<span></span>
<span>{worker.age} yrs</span>
</div>
</div>
</div>
{/* Top Right Availability status and trash remove button */}
<div className="flex flex-col items-end space-y-2">
{/* Top Right Actions & Salary */}
<div className="flex flex-col items-end justify-between self-stretch flex-shrink-0 ml-3">
<button
type="button"
onClick={() => removeWorker(worker.id)}
className="p-2 bg-rose-50 text-rose-600 hover:bg-rose-100 rounded-xl transition-colors border border-rose-100"
className="p-1.5 bg-rose-50 hover:bg-rose-100 border border-rose-100 text-rose-600 rounded-lg transition-colors"
title={t('remove_from_shortlist', 'Remove from shortlist')}
>
<Trash2 className="w-3.5 h-3.5" />
</button>
<div className="text-right mt-auto pt-2">
<div className="text-[#185FA5] text-base font-extrabold tracking-tight">
{worker.salary} <span className="text-[10px] font-bold text-slate-500">AED</span>
</div>
<div className="text-[9px] text-slate-400 font-bold mt-[-2px]">/mo</div>
</div>
</div>
</div>
{/* Card Body */}
<div className="p-6 space-y-4 flex-1 flex flex-col justify-between">
<div className="space-y-4">
{/* Ratings & Cost */}
<div className="flex items-center justify-end text-xs">
<div className="flex items-center space-x-1 font-bold text-slate-800">
<DollarSign className="w-3.5 h-3.5 text-emerald-600" />
<span className="text-sm font-black">{worker.salary} AED</span>
<span className="text-[10px] text-slate-400 font-medium">/mo</span>
</div>
</div>
<div className="p-4 sm:p-5 pt-3 space-y-3 flex-1 flex flex-col justify-between">
<div className="space-y-3">
{/* Details Table */}
<div className="grid grid-cols-2 gap-2.5 p-3.5 bg-slate-50 rounded-xl text-[11px] text-slate-600 font-bold">
<div className="grid grid-cols-2 gap-2 p-2.5 bg-slate-50/70 border border-slate-100/50 rounded-xl text-[10px] text-slate-600 font-bold">
<div className="flex items-center space-x-1.5 truncate">
<Briefcase className="w-3.5 h-3.5 text-slate-400 flex-shrink-0" />
<Briefcase className="w-3 h-3 text-slate-400 flex-shrink-0" />
<span className="truncate">Exp: {worker.experience}</span>
</div>
<div className="flex items-center space-x-1.5 truncate">
<Sparkles className="w-3.5 h-3.5 text-slate-400 flex-shrink-0" />
<span className="truncate uppercase text-[10px]">{worker.preferred_job_type}</span>
<Sparkles className="w-3 h-3 text-slate-400 flex-shrink-0" />
<span className="truncate uppercase text-[9px]">{worker.preferred_job_type}</span>
</div>
<div className="flex items-center space-x-1.5 truncate">
<MapPin className="w-3.5 h-3.5 text-[#185FA5] flex-shrink-0" />
<MapPin className="w-3 h-3 text-[#185FA5] flex-shrink-0" />
<span className="truncate text-[#185FA5]">{worker.preferred_location || 'Not Specified'}</span>
</div>
<div className="flex items-center space-x-1.5 truncate">
<Star className="w-3.5 h-3.5 text-amber-500 flex-shrink-0 fill-amber-500" />
<Star className="w-3 h-3 text-amber-500 flex-shrink-0 fill-amber-500" />
<span className="truncate">{worker.rating || '4.5'} ({worker.reviews_count || '12'})</span>
</div>
</div>
{/* Core exact platform Skills List */}
<div className="space-y-1">
<div className="text-[9px] font-black text-slate-400 uppercase tracking-widest">{t('platform_skills', 'Platform Skills')}</div>
<div className="space-y-0.5">
<div className="text-[8px] font-black text-slate-400 uppercase tracking-widest">{t('platform_skills', 'Platform Skills')}</div>
<div className="flex flex-wrap gap-1">
{worker.skills?.map(skill => (
<span key={skill} className="px-2.5 py-0.5 bg-blue-50 border border-blue-100 text-[#185FA5] text-[9px] font-black rounded-lg uppercase tracking-wider">
{worker.skills?.slice(0, 3).map(skill => (
<span key={skill} className="px-2 py-0.5 bg-blue-50 border border-blue-100 text-[#185FA5] text-[8px] font-black rounded-md uppercase tracking-wider">
{skill}
</span>
))}
{worker.skills?.length > 3 && (
<span className="px-1.5 py-0.5 bg-slate-50 border border-slate-200 text-slate-500 text-[8px] font-black rounded-md">
+{worker.skills.length - 3} More
</span>
)}
</div>
</div>
{/* Languages Badges with Visual Flags */}
<div className="space-y-1">
<div className="text-[9px] font-black text-slate-400 uppercase tracking-widest">{t('languages_spoken', 'Languages Spoken')}</div>
<div className="flex flex-wrap gap-1.5">
<div className="space-y-0.5">
<div className="text-[8px] font-black text-slate-400 uppercase tracking-widest">{t('languages_spoken', 'Languages Spoken')}</div>
<div className="flex flex-wrap gap-1">
{worker.languages?.map(lang => (
<span key={lang} className="text-[9px] bg-slate-100 text-slate-700 px-2 py-0.5 rounded font-bold uppercase flex items-center space-x-1 border border-slate-200">
<span key={lang} className="text-[8px] bg-slate-50 text-slate-600 px-2 py-0.5 rounded font-bold uppercase flex items-center space-x-1 border border-slate-200">
<span>{getLanguageFlag(lang)}</span>
<span>{lang}</span>
</span>
@ -224,12 +222,12 @@ export default function Shortlist({ shortlistedWorkers }) {
</div>
{/* Actions block */}
<div className="pt-4 border-t border-slate-100 space-y-2">
<div className="pt-3 border-t border-slate-100 space-y-2">
<div className="grid grid-cols-2 gap-2">
<button
type="button"
onClick={() => setPreviewWorker(worker)}
className="w-full bg-slate-50 border border-slate-200 hover:bg-slate-100 text-slate-700 rounded-xl h-10 font-bold text-xs flex items-center justify-center transition-colors space-x-1"
className="w-full bg-slate-50 border border-slate-200 hover:bg-slate-100 text-slate-700 rounded-xl h-9 font-bold text-xs flex items-center justify-center transition-colors space-x-1"
>
<Eye className="w-3.5 h-3.5" />
<span>{t('quick_preview', 'Quick Preview')}</span>
@ -238,26 +236,26 @@ export default function Shortlist({ shortlistedWorkers }) {
<button
type="button"
onClick={() => handleToggleComparison(worker.id)}
className={`w-full rounded-xl h-10 font-bold text-xs flex items-center justify-center transition-colors border ${
className={`w-full rounded-xl h-9 font-bold text-xs flex items-center justify-center transition-colors border ${
isComparing
? 'bg-purple-600 border-purple-600 text-white hover:bg-purple-700'
: 'bg-white border-slate-200 hover:bg-slate-50 text-slate-700'
}`}
>
{isComparing ? t('comparing_active', 'Comparing ✓') : t('compare_candidate', 'Compare Candidate')}
{isComparing ? t('comparing_active', 'Comparing ✓') : t('compare_candidate', 'Compare')}
</button>
</div>
<div className="grid grid-cols-2 gap-2">
<Link
href={`/employer/workers/${worker.id}`}
className="w-full bg-slate-100 hover:bg-slate-200 text-slate-700 rounded-xl h-10 font-bold text-xs flex items-center justify-center transition-colors"
className="w-full bg-slate-100 hover:bg-slate-200 text-slate-700 rounded-xl h-9 font-bold text-xs flex items-center justify-center transition-colors"
>
{t('open_profile', 'Open Profile')}
</Link>
<Link
href={`/employer/messages/${worker.id}`}
className="w-full bg-[#185FA5] hover:bg-[#144f8a] text-white rounded-xl h-10 font-bold text-xs flex items-center justify-center space-x-1.5 transition-colors shadow-xs"
className="w-full bg-[#185FA5] hover:bg-[#144f8a] text-white rounded-xl h-9 font-bold text-xs flex items-center justify-center space-x-1.5 transition-colors shadow-xs"
>
<MessageSquare className="w-4 h-4" />
<span>{t('message', 'Message')}</span>

View File

@ -47,6 +47,7 @@ export default function Index({ initialWorkers = [], initialShortlistedIds = [],
const { t } = useTranslation();
// Basic Filters
const [searchQuery, setSearchQuery] = useState('');
const [selectedProfession, setSelectedProfession] = useState('All Professions');
const [selectedNationalities, setSelectedNationalities] = useState([]);
const [selectedGender, setSelectedGender] = useState('All Genders');
const [selectedExperience, setSelectedExperience] = useState('All Experience');
@ -108,9 +109,13 @@ export default function Index({ initialWorkers = [], initialShortlistedIds = [],
const outCountryVal = params.get('out_country');
const visa = params.get('visa_status') || params.get('next_visa_type') || params.get('visa_type');
const gender = params.get('gender');
const prof = params.get('main_profession') || params.get('profession');
let hasAdvanced = false;
if (prof) {
setSelectedProfession(prof);
}
if (nat) {
setSelectedNationalities(nat.split(',').map(x => x.trim()).filter(Boolean));
}
@ -196,6 +201,7 @@ export default function Index({ initialWorkers = [], initialShortlistedIds = [],
const resetFilters = () => {
setSearchQuery('');
setSelectedProfession('All Professions');
setSelectedNationalities([]);
setSelectedGender('All Genders');
setSelectedExperience('All Experience');
@ -221,13 +227,14 @@ export default function Index({ initialWorkers = [], initialShortlistedIds = [],
if (filterAccommodation !== 'All') count++;
if (selectedNationalities.length > 0) count++;
if (selectedGender !== 'All Genders') count++;
if (selectedProfession !== 'All Professions') count++;
if (filterInCountry !== 'All') count++;
if (filterInCountry === 'In Country' && filterVisaType !== 'All') count++;
if (selectedSkills.length > 0) count++;
if (selectedLanguages.length > 0) count++;
if (maxSalary < 5000) count++;
return count;
}, [filterLocation, filterJobType, filterAccommodation, selectedNationalities, selectedGender, filterInCountry, filterVisaType, selectedSkills, selectedLanguages, maxSalary]);
}, [filterLocation, filterJobType, filterAccommodation, selectedNationalities, selectedGender, selectedProfession, filterInCountry, filterVisaType, selectedSkills, selectedLanguages, maxSalary]);
const activeFilterTags = useMemo(() => {
const tags = [];
@ -236,13 +243,14 @@ export default function Index({ initialWorkers = [], initialShortlistedIds = [],
if (filterAccommodation !== 'All') tags.push({ key: 'acc', label: `🏠 ${filterAccommodation.replace('_', '-')}`, clear: () => setFilterAccommodation('All') });
if (selectedNationalities.length > 0) tags.push({ key: 'nat', label: `🌍 ${selectedNationalities.length} Nat`, clear: () => setSelectedNationalities([]) });
if (selectedGender !== 'All Genders') tags.push({ key: 'gender', label: `🚻 ${selectedGender}`, clear: () => setSelectedGender('All Genders') });
if (selectedProfession !== 'All Professions') tags.push({ key: 'profession', label: `🧑‍🔧 ${selectedProfession}`, clear: () => setSelectedProfession('All Professions') });
if (filterInCountry !== 'All') tags.push({ key: 'country', label: `✈️ ${filterInCountry}`, clear: () => { setFilterInCountry('All'); setFilterVisaType('All'); } });
if (filterInCountry === 'In Country' && filterVisaType !== 'All') tags.push({ key: 'visa', label: `🪪 ${filterVisaType}`, clear: () => setFilterVisaType('All') });
if (selectedSkills.length > 0) tags.push({ key: 'skills', label: `🛠 ${selectedSkills.length} skill${selectedSkills.length > 1 ? 's' : ''}`, clear: () => setSelectedSkills([]) });
if (selectedLanguages.length > 0) tags.push({ key: 'lang', label: `🗣 ${selectedLanguages.length} lang`, clear: () => setSelectedLanguages([]) });
if (maxSalary < 5000) tags.push({ key: 'salary', label: `💰 ≤${maxSalary} AED`, clear: () => setMaxSalary(5000) });
return tags;
}, [filterLocation, filterJobType, filterAccommodation, selectedNationalities, selectedGender, filterInCountry, filterVisaType, selectedSkills, selectedLanguages, maxSalary]);
}, [filterLocation, filterJobType, filterAccommodation, selectedNationalities, selectedGender, selectedProfession, filterInCountry, filterVisaType, selectedSkills, selectedLanguages, maxSalary]);
// Filter and Sort Worker List
const filteredWorkers = useMemo(() => {
@ -257,6 +265,7 @@ export default function Index({ initialWorkers = [], initialShortlistedIds = [],
}
// Dropdown filters
if (selectedProfession !== 'All Professions' && (!worker.main_profession || worker.main_profession.toLowerCase() !== selectedProfession.toLowerCase())) return false;
if (selectedNationalities.length > 0 && (!worker.nationality || !selectedNationalities.map(n => n.toLowerCase()).includes(worker.nationality.toLowerCase()))) return false;
if (selectedGender !== 'All Genders' && worker.gender && worker.gender.toLowerCase() !== selectedGender.toLowerCase()) return false;
if (selectedExperience !== 'All Experience' && worker.experience !== selectedExperience) return false;
@ -332,6 +341,7 @@ export default function Index({ initialWorkers = [], initialShortlistedIds = [],
}, [
initialWorkers,
searchQuery,
selectedProfession,
selectedNationalities,
selectedGender,
selectedExperience,
@ -400,6 +410,23 @@ export default function Index({ initialWorkers = [], initialShortlistedIds = [],
activeCount={activeFilterCount}
title="Filter Workers"
>
{/* Profession */}
{filtersMetadata.professions && (
<FilterSection label="Profession">
<FilterSelect
id="filter_main_profession"
value={selectedProfession}
onChange={e => setSelectedProfession(e.target.value)}
>
{filtersMetadata.professions.map(prof => (
<option key={prof} value={prof}>
{prof === 'All Professions' ? 'All Professions' : prof}
</option>
))}
</FilterSelect>
</FilterSection>
)}
{/* Preferred Location */}
<FilterSection label="Preferred Location">
<FilterSelect
@ -648,142 +675,140 @@ export default function Index({ initialWorkers = [], initialShortlistedIds = [],
<div key={worker.id} className="bg-white rounded-2xl border border-slate-200 shadow-sm hover:shadow-md transition-all flex flex-col justify-between overflow-hidden group relative">
{/* Card Header with Photo Option and Availability indicator */}
<div className="bg-slate-50/50 p-6 border-b border-slate-100 flex items-start justify-between relative">
<div className="flex items-center space-x-3.5 min-w-0 flex-1">
<div className="bg-slate-50/40 p-4 sm:p-5 pb-3 border-b border-slate-100 flex items-start justify-between relative">
<div className="flex items-center space-x-3 min-w-0 flex-1">
{/* Photo (optional) container */}
<div className="w-14 h-14 rounded-2xl bg-blue-100 text-[#185FA5] flex items-center justify-center font-bold text-xl border border-blue-200 shadow-inner flex-shrink-0 overflow-hidden relative">
<div className="w-12 h-12 rounded-xl bg-blue-50 text-[#185FA5] flex items-center justify-center font-bold text-lg border border-blue-100 shadow-inner flex-shrink-0 overflow-hidden relative">
{worker.photo ? (
<img src={worker.photo} alt={worker.name} className="w-full h-full object-cover" />
) : (
<User className="w-6 h-6 text-slate-400" />
<User className="w-5 h-5 text-slate-400" />
)}
</div>
<div className="min-w-0 flex-1">
<div className="font-extrabold text-base text-slate-900 group-hover:text-[#185FA5] transition-colors truncate flex items-center space-x-1.5">
<div className="min-w-0 flex-1 space-y-1">
<div className="font-extrabold text-sm sm:text-base text-slate-900 group-hover:text-[#185FA5] transition-colors truncate flex items-center gap-1.5 flex-wrap">
<span>{worker.name}</span>
{false && (
<span className="inline-flex items-center px-1.5 py-0.5 rounded-full text-[8px] font-black uppercase tracking-wider bg-emerald-500 text-white shadow-xs gap-0.5 animate-pulse" title="OCR Verified">
<CheckCircle2 className="w-3 h-3" />
<span>{t('verified')}</span>
{worker.main_profession && (
<span className="inline-flex items-center px-1.5 py-0.5 rounded text-[8px] font-black uppercase tracking-wider bg-blue-50 text-[#185FA5] border border-blue-100">
{worker.main_profession}
</span>
)}
</div>
{/* Passport Status Verification Badge & Visa Status */}
<div className="flex flex-wrap gap-1 mt-0.5">
<div className="flex flex-wrap gap-1">
{worker.visa_expiry_date ? (
<div className={`flex items-center space-x-1 text-[9px] font-black uppercase px-1.5 py-0.5 rounded border ${
<div className={`flex items-center space-x-0.5 text-[8px] font-black uppercase px-1.5 py-0.5 rounded border ${
worker.document_expiry_days !== null && worker.document_expiry_days <= 30
? 'text-rose-700 bg-rose-50 border-rose-200 animate-pulse'
: worker.document_expiry_days !== null && worker.document_expiry_days <= 90
? 'text-amber-700 bg-amber-50 border-amber-200'
: 'text-[#185FA5] bg-blue-50 border-blue-100'
}`}>
<Calendar className="w-3 h-3 flex-shrink-0" />
<Calendar className="w-2.5 h-2.5 flex-shrink-0" />
<span>Visa Exp: {worker.visa_expiry_date}</span>
</div>
) : (
<div className="flex items-center space-x-1 text-[9px] font-black uppercase text-amber-700 bg-amber-50 px-1.5 py-0.5 rounded border border-amber-200">
<Calendar className="w-3 h-3 flex-shrink-0 text-amber-600" />
<div className="flex items-center space-x-0.5 text-[8px] font-black uppercase text-amber-700 bg-amber-50 px-1.5 py-0.5 rounded border border-amber-200">
<Calendar className="w-2.5 h-2.5 flex-shrink-0 text-amber-600" />
<span>Visa Expiry Pending</span>
</div>
)}
{worker.visa_status && (worker.visa_status.toLowerCase().includes('tourist') || worker.visa_status.toLowerCase().includes('visit')) ? (
<div className="flex items-center space-x-1 text-[9px] font-black uppercase text-amber-700 bg-amber-50 px-1.5 py-0.5 rounded border border-amber-200">
<AlertTriangle className="w-3 h-3 text-amber-600 flex-shrink-0 animate-bounce" />
<span>{worker.visa_status} ({t('warning', 'Warning')})</span>
<div className="flex items-center space-x-0.5 text-[8px] font-black uppercase text-amber-700 bg-amber-50 px-1.5 py-0.5 rounded border border-amber-200">
<AlertTriangle className="w-2.5 h-2.5 text-amber-600 flex-shrink-0 animate-bounce" />
<span>{worker.visa_status}</span>
</div>
) : worker.visa_status && worker.visa_status.toLowerCase().includes('cancelled') ? (
<div className="flex items-center space-x-1 text-[9px] font-black uppercase text-rose-700 bg-rose-50 px-1.5 py-0.5 rounded border border-rose-200 animate-pulse">
<AlertTriangle className="w-3 h-3 text-rose-600 flex-shrink-0" />
<span>{worker.visa_status} ({t('warning', 'Warning')})</span>
<div className="flex items-center space-x-0.5 text-[8px] font-black uppercase text-rose-700 bg-rose-50 px-1.5 py-0.5 rounded border border-rose-200 animate-pulse">
<AlertTriangle className="w-2.5 h-2.5 text-rose-600 flex-shrink-0" />
<span>{worker.visa_status}</span>
</div>
) : (
<div className="flex items-center space-x-1 text-[9px] font-black uppercase text-blue-700 bg-blue-50 px-1.5 py-0.5 rounded border border-blue-100">
<div className="flex items-center space-x-0.5 text-[8px] font-black uppercase text-blue-700 bg-blue-50 px-1.5 py-0.5 rounded border border-blue-100">
<span>{worker.visa_status}</span>
</div>
)}
{/* Removed visa expiry status badge for cleaner UI */}
</div>
<div className="flex items-center space-x-1.5 text-xs text-slate-500 mt-1">
<Globe2 className="w-3.5 h-3.5 text-slate-400" />
<span className="font-bold">{worker.nationality}</span>
<div className="flex items-center space-x-1 text-[10px] text-slate-500 font-bold">
<Globe2 className="w-3 h-3 text-slate-400" />
<span>{worker.nationality}</span>
<span></span>
<span>{worker.age} {t('yrs', 'yrs')}</span>
</div>
</div>
</div>
{/* Top Right Availability status and bookmark */}
<div className="flex flex-col items-end space-y-2 flex-shrink-0 ml-4">
{/* Top Right Actions & Salary */}
<div className="flex flex-col items-end justify-between self-stretch flex-shrink-0 ml-3">
<button
type="button"
onClick={() => toggleShortlist(worker.id)}
className={`p-2 rounded-xl transition-all ${
className={`p-1.5 rounded-lg transition-all border ${
isShortlisted
? 'bg-blue-50 text-[#185FA5] hover:bg-blue-100'
: 'bg-white text-slate-400 hover:text-[#185FA5] border border-slate-200'
? 'bg-blue-50 border-blue-200 text-[#185FA5]'
: 'bg-white hover:bg-slate-50 border-slate-200 text-slate-400 hover:text-[#185FA5] shadow-xs'
}`}
>
<Bookmark className={`w-3.5 h-3.5 ${isShortlisted ? 'fill-[#185FA5]' : ''}`} />
</button>
<div className="text-right mt-auto pt-2">
<div className="text-[#185FA5] text-base font-extrabold tracking-tight">
{worker.salary} <span className="text-[10px] font-bold text-slate-500">{t('aed', 'AED')}</span>
</div>
<div className="text-[9px] text-slate-400 font-bold mt-[-2px]">/{t('mo', 'mo')}</div>
</div>
</div>
</div>
{/* Card Body */}
<div className="p-6 space-y-4 flex-1 flex flex-col justify-between">
<div className="space-y-4">
{/* Ratings & Cost */}
<div className="flex items-center justify-end text-xs">
<div className="flex items-center space-x-1 font-bold text-slate-800">
<DollarSign className="w-3.5 h-3.5 text-emerald-600" />
<span className="text-sm font-black">{worker.salary} {t('aed', 'AED')}</span>
<span className="text-[10px] text-slate-400 font-medium">/{t('mo', 'mo')}</span>
</div>
</div>
<div className="p-4 sm:p-5 pt-3 space-y-3 flex-1 flex flex-col justify-between">
<div className="space-y-3">
{/* Details Table */}
<div className="grid grid-cols-2 gap-2.5 p-3.5 bg-slate-50 rounded-xl text-[11px] text-slate-600 font-bold">
<div className="grid grid-cols-2 gap-2 p-2.5 bg-slate-50/70 border border-slate-100/50 rounded-xl text-[10px] text-slate-600 font-bold">
<div className="flex items-center space-x-1.5 truncate">
<Briefcase className="w-3.5 h-3.5 text-slate-400 flex-shrink-0" />
<Briefcase className="w-3 h-3 text-slate-400 flex-shrink-0" />
<span className="truncate">{t('experience', 'Exp')}: {worker.experience}</span>
</div>
<div className="flex items-center space-x-1.5 truncate">
<Sparkles className="w-3.5 h-3.5 text-slate-400 flex-shrink-0" />
<span className="truncate uppercase text-[10px]">{worker.preferred_job_type}</span>
<Sparkles className="w-3 h-3 text-slate-400 flex-shrink-0" />
<span className="truncate uppercase text-[9px]">{worker.preferred_job_type}</span>
</div>
<div className="flex items-center space-x-1.5 truncate">
<MapPin className="w-3.5 h-3.5 text-[#185FA5] flex-shrink-0" />
<MapPin className="w-3 h-3 text-[#185FA5] flex-shrink-0" />
<span className="truncate text-[#185FA5]">{worker.preferred_location || 'Not Specified'}</span>
</div>
<div className="flex items-center space-x-1.5 truncate">
<Star className="w-3.5 h-3.5 text-amber-500 flex-shrink-0 fill-amber-500" />
<Star className="w-3 h-3 text-amber-500 flex-shrink-0 fill-amber-500" />
<span className="truncate">{worker.rating} ({worker.reviews_count})</span>
</div>
</div>
{/* Core exact platform Skills List */}
<div className="space-y-1">
<div className="text-[9px] font-black text-slate-400 uppercase tracking-widest">{t('platform_skills', 'Platform Skills')}</div>
<div className="space-y-0.5">
<div className="text-[8px] font-black text-slate-400 uppercase tracking-widest">{t('platform_skills', 'Platform Skills')}</div>
<div className="flex flex-wrap gap-1">
{worker.skills?.map(skill => (
<span key={skill} className="px-2.5 py-0.5 bg-blue-50 border border-blue-100 text-[#185FA5] text-[9px] font-black rounded-lg uppercase tracking-wider">
{worker.skills?.slice(0, 3).map(skill => (
<span key={skill} className="px-2 py-0.5 bg-blue-50 border border-blue-100 text-[#185FA5] text-[8px] font-black rounded-md uppercase tracking-wider">
{skill}
</span>
))}
{worker.skills?.length > 3 && (
<span className="px-1.5 py-0.5 bg-slate-50 border border-slate-200 text-slate-500 text-[8px] font-black rounded-md">
+{worker.skills.length - 3} More
</span>
)}
</div>
</div>
{/* Languages Badges with Visual Flags */}
<div className="space-y-1">
<div className="text-[9px] font-black text-slate-400 uppercase tracking-widest">{t('languages_spoken', 'Languages Spoken')}</div>
<div className="flex flex-wrap gap-1.5">
<div className="space-y-0.5">
<div className="text-[8px] font-black text-slate-400 uppercase tracking-widest">{t('languages_spoken', 'Languages Spoken')}</div>
<div className="flex flex-wrap gap-1">
{worker.languages?.map(lang => (
<span key={lang} className="text-[9px] bg-slate-100 text-slate-700 px-2 py-0.5 rounded font-bold uppercase flex items-center space-x-1 border border-slate-200">
<span key={lang} className="text-[8px] bg-slate-50 text-slate-600 px-2 py-0.5 rounded font-bold uppercase flex items-center space-x-1 border border-slate-200">
<span>{getLanguageFlag(lang)}</span>
<span>{lang}</span>
</span>
@ -793,12 +818,12 @@ export default function Index({ initialWorkers = [], initialShortlistedIds = [],
</div>
{/* Actions block */}
<div className="pt-4 border-t border-slate-100 space-y-2">
<div className="pt-3 border-t border-slate-100 space-y-2">
<div className="grid grid-cols-2 gap-2">
<button
type="button"
onClick={() => setPreviewWorker(worker)}
className="w-full bg-slate-50 border border-slate-200 hover:bg-slate-100 text-slate-700 rounded-xl h-10 font-bold text-xs flex items-center justify-center transition-colors space-x-1"
className="w-full bg-slate-50 border border-slate-200 hover:bg-slate-100 text-slate-700 rounded-xl h-9 font-bold text-xs flex items-center justify-center transition-colors space-x-1"
>
<Eye className="w-3.5 h-3.5" />
<span>{t('quick_preview')}</span>
@ -807,22 +832,21 @@ export default function Index({ initialWorkers = [], initialShortlistedIds = [],
<button
type="button"
onClick={() => handleToggleComparison(worker.id)}
className={`w-full rounded-xl h-10 font-bold text-xs flex items-center justify-center transition-colors border ${
className={`w-full rounded-xl h-9 font-bold text-xs flex items-center justify-center transition-colors border ${
isComparing
? 'bg-purple-600 border-purple-600 text-white hover:bg-purple-700'
: 'bg-white border-slate-200 hover:bg-slate-50 text-slate-700'
}`}
>
{isComparing ? t('comparing', 'Comparing ✓') : t('compare_candidate', 'Compare Candidate')}
{isComparing ? t('comparing', 'Comparing ✓') : t('compare_candidate', 'Compare')}
</button>
</div>
<Link
href={`/employer/workers/${worker.id}`}
className="w-full bg-[#185FA5] hover:bg-[#144f8a] text-white rounded-xl h-10 font-bold text-xs flex items-center justify-center transition-colors shadow-xs"
className="w-full bg-[#185FA5] hover:bg-[#144f8a] text-white rounded-xl h-9 font-bold text-xs flex items-center justify-center transition-colors shadow-xs"
>
View Profile
</Link>
</div>
</div>
@ -947,8 +971,12 @@ export default function Index({ initialWorkers = [], initialShortlistedIds = [],
<div>
<div className="flex items-center space-x-1">
<h4 className="font-extrabold text-lg text-slate-900">{previewWorker.name}</h4>
</div>
{previewWorker.main_profession && (
<div className="text-[10px] font-black text-[#185FA5] bg-blue-50 border border-blue-100 px-2 py-0.5 rounded-md inline-block w-fit mb-1">
{previewWorker.main_profession}
</div>
)}
{previewWorker.visa_expiry_date ? (
<div className={`flex items-center space-x-1 font-black text-[9px] uppercase tracking-wider border px-1.5 py-0.5 rounded w-fit ${

View File

@ -205,8 +205,12 @@ export default function Show({ worker }) {
<div className="space-y-1.5">
<div className="flex items-center space-x-2">
<h1 className="text-2xl sm:text-3xl font-extrabold tracking-tight text-slate-900">{worker.name}</h1>
</div>
{worker.main_profession && (
<div className="text-[11px] font-black text-[#185FA5] bg-blue-50 border border-blue-100 px-2.5 py-1 rounded-lg uppercase tracking-wider w-fit">
{worker.main_profession}
</div>
)}
{/* Passport verification status bar & Visa Status */}
<div className="flex flex-wrap gap-2">
@ -314,6 +318,16 @@ export default function Show({ worker }) {
<span className="font-extrabold text-slate-900 text-xs">{worker.name}</span>
</div>
{worker.main_profession && (
<div className="flex items-center justify-between pb-3 border-b border-slate-200">
<div className="flex items-center space-x-2 text-xs text-slate-600 font-bold">
<Briefcase className="w-4 h-4 text-slate-500" />
<span>{t('main_profession', 'Main Profession')}</span>
</div>
<span className="font-extrabold text-slate-900 text-xs">{worker.main_profession}</span>
</div>
)}
<div className="flex items-center justify-between pb-3 border-b border-slate-200">
<div className="flex items-center space-x-2 text-xs text-slate-600 font-bold">
<Phone className="w-4 h-4 text-slate-500" />

View File

@ -561,6 +561,16 @@ public function test_employer_can_get_plans_and_pay_with_numeric_id()
$this->assertIsInt($plan['id']);
}
// Create sponsor record for payment verification
\App\Models\Sponsor::create([
'email' => $this->employer->email,
'full_name' => $this->employer->name,
'mobile' => '+971501112222',
'password' => $this->employer->password,
'is_verified' => true,
'status' => 'active',
]);
// 2. Submit payment with a numeric plan ID (e.g. 3)
$responsePayment = $this->postJson('/api/employers/payment', [
'email' => $this->employer->email,

View File

@ -154,6 +154,7 @@ public function test_s1_complete_basic_profile_setup()
'nationality' => 'Indian',
'language' => 'HI',
'gender' => 'male',
'main_profession' => 'Nanny',
'skills' => [],
]);
@ -180,6 +181,7 @@ public function test_s1_complete_basic_profile_setup()
'phone' => $phone,
'language' => 'HI',
'gender' => 'male',
'main_profession' => 'Nanny',
'verified' => false,
'status' => 'active',
]);
@ -386,6 +388,7 @@ public function test_register_with_new_api_fields()
'in_country' => 'true',
'visa_status' => 'Tourist Visa',
'preferred_job_type' => 'full-time',
'main_profession' => 'Housemaid',
'gender' => 'male',
'live_in_out' => 'live_out',
'preferred_location' => 'Dubai Marina',
@ -399,6 +402,7 @@ public function test_register_with_new_api_fields()
'in_country' => true,
'visa_status' => 'Tourist Visa',
'preferred_job_type' => 'full-time',
'main_profession' => 'Housemaid',
'gender' => 'male',
'live_in_out' => 'live_out',
'preferred_location' => 'Dubai Marina',