881 lines
54 KiB
JavaScript
881 lines
54 KiB
JavaScript
import React, { useState, useMemo, useEffect } from 'react';
|
|
import { Head, Link, router } from '@inertiajs/react';
|
|
import EmployerLayout from '../../../Layouts/EmployerLayout';
|
|
import {
|
|
Search,
|
|
SlidersHorizontal,
|
|
CheckCircle2,
|
|
MapPin,
|
|
Briefcase,
|
|
DollarSign,
|
|
Calendar,
|
|
Bookmark,
|
|
RotateCcw,
|
|
HeartHandshake,
|
|
Globe2,
|
|
Sparkles,
|
|
Filter,
|
|
ArrowUpDown,
|
|
Check,
|
|
X,
|
|
Eye,
|
|
ChevronDown,
|
|
Layers,
|
|
AlertTriangle,
|
|
Star,
|
|
CheckCircle,
|
|
ShieldCheck,
|
|
User
|
|
} from 'lucide-react';
|
|
|
|
const getLanguageFlag = (lang) => {
|
|
const flags = {
|
|
'English': '🇬🇧',
|
|
'Arabic': '🇦🇪',
|
|
'Tagalog': '🇵🇭',
|
|
'Hindi': '🇮🇳',
|
|
'Swahili': '🇰🇪',
|
|
'French': '🇫🇷',
|
|
'Indonesian': '🇮🇩'
|
|
};
|
|
return flags[lang] || '🌐';
|
|
};
|
|
|
|
export default function Index({ initialWorkers = [], initialShortlistedIds = [], filtersMetadata = {} }) {
|
|
// Basic Filters
|
|
const [searchQuery, setSearchQuery] = useState('');
|
|
const [selectedCategory, setSelectedCategory] = useState('All Categories');
|
|
const [selectedNationality, setSelectedNationality] = useState('All Nationalities');
|
|
const [selectedAvailability, setSelectedAvailability] = useState('All Availabilities');
|
|
const [selectedExperience, setSelectedExperience] = useState('All Experience');
|
|
const [selectedReligion, setSelectedReligion] = useState('All Religions');
|
|
const [maxSalary, setMaxSalary] = useState(3000);
|
|
|
|
// Advanced Multi-Select Filters & Sorting
|
|
const [selectedLanguages, setSelectedLanguages] = useState([]);
|
|
const [selectedVisaStatuses, setSelectedVisaStatuses] = useState([]);
|
|
const [selectedSkills, setSelectedSkills] = useState([]);
|
|
const [selectedWorkTypes, setSelectedWorkTypes] = useState([]);
|
|
const [sortBy, setSortBy] = useState('default');
|
|
|
|
// UI Helpers
|
|
const [isLangOpen, setIsLangOpen] = useState(false);
|
|
const [isVisaOpen, setIsVisaOpen] = useState(false);
|
|
const [isSkillsOpen, setIsSkillsOpen] = useState(false);
|
|
const [isWorkTypeOpen, setIsWorkTypeOpen] = useState(false);
|
|
|
|
// Pagination
|
|
const [visibleCount, setVisibleCount] = useState(6);
|
|
|
|
// Shortlist / Favorites
|
|
const [shortlistedIds, setShortlistedIds] = useState(initialShortlistedIds || []);
|
|
|
|
// Saved Presets
|
|
const [savedPresets, setSavedPresets] = useState([
|
|
{ id: 1, name: 'Live-in Baby Care', category: 'Childcare', workType: 'Live-in', minSalary: 1200 },
|
|
{ id: 2, name: 'Experienced Driver', category: 'Driver', experience: '5+ Years' }
|
|
]);
|
|
const [presetNameInput, setPresetNameInput] = useState('');
|
|
const [showPresetModal, setShowPresetModal] = useState(false);
|
|
|
|
// Worker Comparison & Quick Preview
|
|
const [comparisonIds, setComparisonIds] = useState([]);
|
|
const [previewWorker, setPreviewWorker] = useState(null);
|
|
|
|
// Retrieve active preset query if present
|
|
useEffect(() => {
|
|
const params = new URLSearchParams(window.location.search);
|
|
const cat = params.get('category');
|
|
const nat = params.get('nationality');
|
|
const avail = params.get('availability');
|
|
const sal = params.get('max_salary');
|
|
|
|
if (cat) setSelectedCategory(cat);
|
|
if (nat) setSelectedNationality(nat);
|
|
if (avail) setSelectedAvailability(avail);
|
|
if (sal) setMaxSalary(Number(sal));
|
|
}, []);
|
|
|
|
const toggleShortlist = (id) => {
|
|
if (shortlistedIds.includes(id)) {
|
|
setShortlistedIds(shortlistedIds.filter(i => i !== id));
|
|
} else {
|
|
setShortlistedIds([...shortlistedIds, id]);
|
|
}
|
|
|
|
router.post('/employer/shortlist/toggle', { worker_id: id }, {
|
|
preserveScroll: true,
|
|
onSuccess: (page) => {
|
|
if (page.props.initialShortlistedIds) {
|
|
setShortlistedIds(page.props.initialShortlistedIds);
|
|
}
|
|
}
|
|
});
|
|
};
|
|
|
|
const toggleMultiSelect = (item, selectedList, setSelectedList) => {
|
|
if (selectedList.includes(item)) {
|
|
setSelectedList(selectedList.filter(x => x !== item));
|
|
} else {
|
|
setSelectedList([...selectedList, item]);
|
|
}
|
|
};
|
|
|
|
const applyPreset = (preset) => {
|
|
resetFilters();
|
|
if (preset.category) setSelectedCategory(preset.category);
|
|
if (preset.workType) setSelectedWorkTypes([preset.workType]);
|
|
if (preset.experience) setSelectedExperience(preset.experience);
|
|
};
|
|
|
|
const saveCurrentPreset = (e) => {
|
|
e.preventDefault();
|
|
if (!presetNameInput.trim()) return;
|
|
|
|
const newPreset = {
|
|
id: Date.now(),
|
|
name: presetNameInput,
|
|
category: selectedCategory !== 'All Categories' ? selectedCategory : null,
|
|
workType: selectedWorkTypes.length > 0 ? selectedWorkTypes[0] : null,
|
|
experience: selectedExperience !== 'All Experience' ? selectedExperience : null,
|
|
};
|
|
|
|
setSavedPresets([...savedPresets, newPreset]);
|
|
setPresetNameInput('');
|
|
setShowPresetModal(false);
|
|
};
|
|
|
|
const resetFilters = () => {
|
|
setSearchQuery('');
|
|
setSelectedCategory('All Categories');
|
|
setSelectedNationality('All Nationalities');
|
|
setSelectedAvailability('All Availabilities');
|
|
setSelectedExperience('All Experience');
|
|
setSelectedReligion('All Religions');
|
|
setMaxSalary(3000);
|
|
setSelectedLanguages([]);
|
|
setSelectedVisaStatuses([]);
|
|
setSelectedSkills([]);
|
|
setSelectedWorkTypes([]);
|
|
setSortBy('default');
|
|
setVisibleCount(6);
|
|
};
|
|
|
|
// Filter and Sort Worker List
|
|
const filteredWorkers = useMemo(() => {
|
|
let workers = initialWorkers.filter(worker => {
|
|
// Search Query
|
|
if (searchQuery.trim() !== '') {
|
|
const query = searchQuery.toLowerCase();
|
|
const matchName = worker.name.toLowerCase().includes(query);
|
|
const matchBio = worker.bio?.toLowerCase().includes(query) || false;
|
|
const matchSkills = worker.skills?.some(s => s.toLowerCase().includes(query)) || false;
|
|
if (!matchName && !matchBio && !matchSkills) return false;
|
|
}
|
|
|
|
// Dropdown filters
|
|
if (selectedCategory !== 'All Categories' && worker.category !== selectedCategory) return false;
|
|
if (selectedNationality !== 'All Nationalities' && worker.nationality !== selectedNationality) return false;
|
|
if (selectedAvailability !== 'All Availabilities' && worker.availability_status !== selectedAvailability) return false;
|
|
if (selectedExperience !== 'All Experience' && worker.experience !== selectedExperience) return false;
|
|
if (selectedReligion !== 'All Religions' && worker.religion !== selectedReligion) return false;
|
|
if (worker.salary > maxSalary) return false;
|
|
|
|
// Multi-select filters
|
|
if (selectedLanguages.length > 0) {
|
|
const hasLang = worker.languages?.some(l => selectedLanguages.includes(l));
|
|
if (!hasLang) return false;
|
|
}
|
|
if (selectedSkills.length > 0) {
|
|
const hasSkill = worker.skills?.some(s => selectedSkills.includes(s.toLowerCase()));
|
|
if (!hasSkill) return false;
|
|
}
|
|
if (selectedVisaStatuses.length > 0) {
|
|
if (!selectedVisaStatuses.includes(worker.visa_status)) return false;
|
|
}
|
|
if (selectedWorkTypes.length > 0 && !selectedWorkTypes.includes(worker.preferred_job_type)) return false;
|
|
|
|
return true;
|
|
});
|
|
|
|
// Sorting
|
|
if (sortBy === 'salary_asc') {
|
|
workers.sort((a, b) => a.salary - b.salary);
|
|
} else if (sortBy === 'salary_desc') {
|
|
workers.sort((a, b) => b.salary - a.salary);
|
|
} else if (sortBy === 'rating') {
|
|
workers.sort((a, b) => b.rating - a.rating);
|
|
} else if (sortBy === 'experience') {
|
|
workers.sort((a, b) => b.age - a.age); // approximate experience by age
|
|
}
|
|
|
|
return workers;
|
|
}, [
|
|
initialWorkers,
|
|
searchQuery,
|
|
selectedCategory,
|
|
selectedNationality,
|
|
selectedAvailability,
|
|
selectedExperience,
|
|
selectedReligion,
|
|
maxSalary,
|
|
selectedLanguages,
|
|
selectedVisaStatuses,
|
|
selectedSkills,
|
|
selectedWorkTypes,
|
|
sortBy
|
|
]);
|
|
|
|
const paginatedWorkers = useMemo(() => {
|
|
return filteredWorkers.slice(0, visibleCount);
|
|
}, [filteredWorkers, visibleCount]);
|
|
|
|
const handleToggleComparison = (id) => {
|
|
if (comparisonIds.includes(id)) {
|
|
setComparisonIds(comparisonIds.filter(i => i !== id));
|
|
} else {
|
|
if (comparisonIds.length >= 3) {
|
|
alert('You can compare a maximum of 3 candidates.');
|
|
return;
|
|
}
|
|
setComparisonIds([...comparisonIds, id]);
|
|
}
|
|
};
|
|
|
|
const comparedWorkers = useMemo(() => {
|
|
return initialWorkers.filter(w => comparisonIds.includes(w.id));
|
|
}, [initialWorkers, comparisonIds]);
|
|
|
|
return (
|
|
<EmployerLayout title="Find Vetted Domestic Workers">
|
|
<Head title="Find Workers - Domestic Worker Platform" />
|
|
|
|
<div className="space-y-8 select-none pb-16">
|
|
|
|
{/* 1. Header Banner */}
|
|
<div className="bg-white rounded-3xl p-8 border border-slate-200 shadow-sm relative overflow-hidden flex flex-col md:flex-row md:items-center justify-between gap-6">
|
|
<div className="absolute top-0 right-0 w-48 h-48 bg-blue-50 rounded-full -mr-16 -mt-16 opacity-40 pointer-events-none" />
|
|
|
|
<div className="space-y-2 relative z-10">
|
|
<div className="flex items-center space-x-2">
|
|
<span className="bg-blue-50 text-[#185FA5] px-3 py-1 rounded-xl text-[10px] font-black uppercase tracking-wider border border-blue-100 flex items-center space-x-1">
|
|
<HeartHandshake className="w-3.5 h-3.5" />
|
|
<span>No Middlemen Platform</span>
|
|
</span>
|
|
</div>
|
|
<h2 className="text-2xl font-black text-slate-900 tracking-tight leading-none">Find Verified Candidates</h2>
|
|
<p className="text-slate-500 font-bold text-xs">Instantly connect with UAE vetted, background-checked domestic workers.</p>
|
|
</div>
|
|
|
|
</div>
|
|
|
|
{/* Filter and Search Panel */}
|
|
<div className="bg-white p-6 rounded-2xl border border-slate-200 shadow-sm space-y-4">
|
|
<div className="flex flex-col lg:flex-row lg:items-center justify-between gap-4">
|
|
{/* Core Search bar */}
|
|
<div className="relative flex-1">
|
|
<Search className="absolute left-3.5 top-1/2 -translate-y-1/2 w-5 h-5 text-slate-400" />
|
|
<input
|
|
type="text"
|
|
value={searchQuery}
|
|
onChange={(e) => setSearchQuery(e.target.value)}
|
|
placeholder="Search by name, skills, bio (e.g. Cooking, Childcare, Gardening, Driving)..."
|
|
className="w-full pl-11 pr-4 py-3 rounded-xl border border-slate-300 text-sm focus:outline-none focus:ring-2 focus:ring-[#185FA5]/20 focus:border-[#185FA5]"
|
|
/>
|
|
</div>
|
|
|
|
{/* Sort & Reset Actions */}
|
|
<div className="flex items-center space-x-3 w-full lg:w-auto justify-end">
|
|
{/* Sort Selector */}
|
|
<div className="relative flex items-center bg-slate-50 rounded-xl border border-slate-200 px-3 py-2 text-xs font-bold text-slate-700">
|
|
<ArrowUpDown className="w-4 h-4 text-slate-400 mr-2" />
|
|
<select
|
|
value={sortBy}
|
|
onChange={(e) => setSortBy(e.target.value)}
|
|
className="bg-transparent focus:outline-none cursor-pointer"
|
|
>
|
|
<option value="default">Default Match</option>
|
|
<option value="salary_asc">Salary: Low to High</option>
|
|
<option value="salary_desc">Salary: High to Low</option>
|
|
<option value="rating">Rating: High to Low</option>
|
|
<option value="experience">Experience Level</option>
|
|
</select>
|
|
</div>
|
|
|
|
<button
|
|
type="button"
|
|
onClick={resetFilters}
|
|
className="flex items-center justify-center space-x-1.5 px-4 py-2.5 rounded-xl border border-slate-200 hover:bg-slate-50 text-xs font-semibold text-slate-600 transition-colors"
|
|
>
|
|
<RotateCcw className="w-4 h-4 text-slate-400" />
|
|
<span>Reset</span>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Primary filters grid */}
|
|
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-5 gap-3 pt-4 border-t border-slate-100">
|
|
{/* Nationality */}
|
|
<div>
|
|
<label className="block text-[10px] font-black text-slate-400 uppercase tracking-widest mb-1.5">Nationality</label>
|
|
<select
|
|
value={selectedNationality}
|
|
onChange={(e) => setSelectedNationality(e.target.value)}
|
|
className="w-full px-3 py-2.5 rounded-xl border border-slate-200 text-xs bg-slate-50/50 focus:bg-white focus:outline-none focus:border-[#185FA5] font-semibold text-slate-700 cursor-pointer"
|
|
>
|
|
{filtersMetadata.nationalities?.map(nat => (
|
|
<option key={nat} value={nat}>{nat}</option>
|
|
))}
|
|
</select>
|
|
</div>
|
|
|
|
{/* Skills */}
|
|
<div className="relative">
|
|
<label className="block text-[10px] font-black text-slate-400 uppercase tracking-widest mb-1.5">Skills</label>
|
|
<button
|
|
type="button"
|
|
onClick={() => {
|
|
setIsSkillsOpen(!isSkillsOpen);
|
|
setIsLangOpen(false);
|
|
setIsVisaOpen(false);
|
|
}}
|
|
className="w-full px-3 py-2.5 rounded-xl border border-slate-200 text-xs bg-slate-50/50 flex justify-between items-center font-bold text-slate-600"
|
|
>
|
|
<span className="truncate">
|
|
{selectedSkills.length === 0
|
|
? 'Select Skills'
|
|
: `${selectedSkills.length} Selected`}
|
|
</span>
|
|
<ChevronDown className="w-4 h-4 ml-1 flex-shrink-0" />
|
|
</button>
|
|
{isSkillsOpen && (
|
|
<div className="absolute left-0 right-0 mt-1 bg-white border border-slate-200 shadow-lg rounded-xl p-3 z-30 space-y-1.5 max-h-60 overflow-y-auto">
|
|
{filtersMetadata.skills?.slice(1).map(skill => (
|
|
<label key={skill} className="flex items-center space-x-2 text-xs font-bold text-slate-600 cursor-pointer">
|
|
<input
|
|
type="checkbox"
|
|
checked={selectedSkills.includes(skill.toLowerCase())}
|
|
onChange={() => toggleMultiSelect(skill.toLowerCase(), selectedSkills, setSelectedSkills)}
|
|
className="rounded text-[#185FA5] focus:ring-[#185FA5]"
|
|
/>
|
|
<span className="capitalize">{skill}</span>
|
|
</label>
|
|
))}
|
|
</div>
|
|
)}
|
|
</div>
|
|
|
|
{/* Language */}
|
|
<div className="relative">
|
|
<label className="block text-[10px] font-black text-slate-400 uppercase tracking-widest mb-1.5">Language</label>
|
|
<button
|
|
type="button"
|
|
onClick={() => {
|
|
setIsLangOpen(!isLangOpen);
|
|
setIsSkillsOpen(false);
|
|
setIsVisaOpen(false);
|
|
}}
|
|
className="w-full px-3 py-2.5 rounded-xl border border-slate-200 text-xs bg-slate-50/50 flex justify-between items-center font-bold text-slate-600"
|
|
>
|
|
<span className="truncate">
|
|
{selectedLanguages.length === 0
|
|
? 'Select Languages'
|
|
: `${selectedLanguages.length} Selected`}
|
|
</span>
|
|
<ChevronDown className="w-4 h-4 ml-1 flex-shrink-0" />
|
|
</button>
|
|
{isLangOpen && (
|
|
<div className="absolute left-0 right-0 mt-1 bg-white border border-slate-200 shadow-lg rounded-xl p-3 z-30 space-y-1.5">
|
|
{filtersMetadata.languages?.slice(1).map(lang => (
|
|
<label key={lang} className="flex items-center space-x-2 text-xs font-bold text-slate-600 cursor-pointer">
|
|
<input
|
|
type="checkbox"
|
|
checked={selectedLanguages.includes(lang)}
|
|
onChange={() => toggleMultiSelect(lang, selectedLanguages, setSelectedLanguages)}
|
|
className="rounded text-[#185FA5] focus:ring-[#185FA5]"
|
|
/>
|
|
<span>{lang}</span>
|
|
</label>
|
|
))}
|
|
</div>
|
|
)}
|
|
</div>
|
|
|
|
{/* Availability */}
|
|
<div>
|
|
<label className="block text-[10px] font-black text-slate-400 uppercase tracking-widest mb-1.5">Availability</label>
|
|
<select
|
|
value={selectedAvailability}
|
|
onChange={(e) => setSelectedAvailability(e.target.value)}
|
|
className="w-full px-3 py-2.5 rounded-xl border border-slate-200 text-xs bg-slate-50/50 focus:bg-white focus:outline-none focus:border-[#185FA5] font-semibold text-slate-700 cursor-pointer"
|
|
>
|
|
{filtersMetadata.availabilities?.map(av => (
|
|
<option key={av} value={av}>{av}</option>
|
|
))}
|
|
</select>
|
|
</div>
|
|
|
|
{/* Visa Status */}
|
|
<div className="relative">
|
|
<label className="block text-[10px] font-black text-slate-400 uppercase tracking-widest mb-1.5">Visa Status</label>
|
|
<button
|
|
type="button"
|
|
onClick={() => {
|
|
setIsVisaOpen(!isVisaOpen);
|
|
setIsSkillsOpen(false);
|
|
setIsLangOpen(false);
|
|
}}
|
|
className="w-full px-3 py-2.5 rounded-xl border border-slate-200 text-xs bg-slate-50/50 flex justify-between items-center font-bold text-slate-600"
|
|
>
|
|
<span className="truncate">
|
|
{selectedVisaStatuses.length === 0
|
|
? 'Select Visa Status'
|
|
: `${selectedVisaStatuses.length} Selected`}
|
|
</span>
|
|
<ChevronDown className="w-4 h-4 ml-1 flex-shrink-0" />
|
|
</button>
|
|
{isVisaOpen && (
|
|
<div className="absolute left-0 right-0 mt-1 bg-white border border-slate-200 shadow-lg rounded-xl p-3 z-30 space-y-1.5">
|
|
{filtersMetadata.visaStatuses?.slice(1).map(status => (
|
|
<label key={status} className="flex items-center space-x-2 text-xs font-bold text-slate-600 cursor-pointer">
|
|
<input
|
|
type="checkbox"
|
|
checked={selectedVisaStatuses.includes(status)}
|
|
onChange={() => toggleMultiSelect(status, selectedVisaStatuses, setSelectedVisaStatuses)}
|
|
className="rounded text-[#185FA5] focus:ring-[#185FA5]"
|
|
/>
|
|
<span>{status}</span>
|
|
</label>
|
|
))}
|
|
</div>
|
|
)}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Listing metadata bar */}
|
|
<div className="flex items-center justify-between px-1">
|
|
<div className="text-xs font-semibold text-slate-500">
|
|
Found <span className="text-slate-900 font-bold">{filteredWorkers.length}</span> domestic workers matching search
|
|
</div>
|
|
<div className="flex items-center space-x-2 text-xs font-medium text-slate-600">
|
|
<CheckCircle className="w-3.5 h-3.5 text-emerald-600" />
|
|
<span>MOHRE UAE Direct Sponsoring Active</span>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Marketplace Workers Grid */}
|
|
{paginatedWorkers.length > 0 ? (
|
|
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
|
|
{paginatedWorkers.map((worker) => {
|
|
const isShortlisted = shortlistedIds.includes(worker.id);
|
|
const isComparing = comparisonIds.includes(worker.id);
|
|
|
|
return (
|
|
<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">
|
|
{/* 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">
|
|
{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" />
|
|
)}
|
|
</div>
|
|
|
|
<div className="truncate">
|
|
<div className="font-extrabold text-base text-slate-900 group-hover:text-[#185FA5] transition-colors truncate flex items-center space-x-1.5">
|
|
<span>{worker.name}</span>
|
|
{worker.verified && (
|
|
<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>Verified</span>
|
|
</span>
|
|
)}
|
|
</div>
|
|
|
|
{/* Emirates ID Status Verification Badge & Visa Status */}
|
|
<div className="flex flex-wrap gap-1 mt-0.5">
|
|
<div className="flex items-center space-x-1 text-[9px] font-black uppercase text-emerald-700 bg-emerald-50 px-1.5 py-0.5 rounded border border-emerald-100">
|
|
<ShieldCheck className="w-3 h-3 text-emerald-600 flex-shrink-0" />
|
|
<span>{worker.emirates_id_status}</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} (Warning)</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} (Warning)</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">
|
|
<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>
|
|
<span>•</span>
|
|
<span>{worker.age} yrs</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Top Right Availability status and bookmark */}
|
|
<div className="flex flex-col items-end space-y-2">
|
|
<span className={`px-2 py-0.5 text-[8px] font-black rounded-lg uppercase tracking-wider border ${
|
|
worker.availability_status === 'Active' ? 'bg-emerald-50 text-emerald-700 border-emerald-200' :
|
|
worker.availability_status === 'Hired' ? 'bg-blue-50 text-blue-700 border-blue-200' :
|
|
'bg-slate-100 text-slate-600 border-slate-200'
|
|
}`}>
|
|
{worker.availability_status}
|
|
</span>
|
|
|
|
<button
|
|
type="button"
|
|
onClick={() => toggleShortlist(worker.id)}
|
|
className={`p-2 rounded-xl transition-all ${
|
|
isShortlisted
|
|
? 'bg-blue-50 text-[#185FA5] hover:bg-blue-100'
|
|
: 'bg-white text-slate-400 hover:text-[#185FA5] border border-slate-200'
|
|
}`}
|
|
>
|
|
<Bookmark className={`w-3.5 h-3.5 ${isShortlisted ? 'fill-[#185FA5]' : ''}`} />
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Card Body */}
|
|
<div className="p-6 space-y-4 flex-1 flex flex-col justify-between">
|
|
<div className="space-y-4">
|
|
|
|
{/* Category Pill, Ratings & Cost */}
|
|
<div className="flex items-center justify-between text-xs">
|
|
<span className="px-2.5 py-1 bg-blue-50 text-[#185FA5] font-black rounded-lg border border-blue-100 uppercase tracking-wider text-[9px]">
|
|
{worker.category}
|
|
</span>
|
|
<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>
|
|
|
|
|
|
{/* 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="flex items-center space-x-1.5 truncate">
|
|
<Briefcase className="w-3.5 h-3.5 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>
|
|
</div>
|
|
<div className="flex items-center space-x-1.5 truncate col-span-2">
|
|
<Star className="w-3.5 h-3.5 text-amber-500 flex-shrink-0 fill-amber-500" />
|
|
<span className="truncate">{worker.rating} ({worker.reviews_count} reviews)</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">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">
|
|
{skill}
|
|
</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">Languages Spoken</div>
|
|
<div className="flex flex-wrap gap-1.5">
|
|
{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>{getLanguageFlag(lang)}</span>
|
|
<span>{lang}</span>
|
|
</span>
|
|
))}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Actions block */}
|
|
<div className="pt-4 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"
|
|
>
|
|
<Eye className="w-3.5 h-3.5" />
|
|
<span>Quick Preview</span>
|
|
</button>
|
|
|
|
<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 ${
|
|
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 ? 'Comparing ✓' : 'Compare Candidate'}
|
|
</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"
|
|
>
|
|
Open Full Profile
|
|
</Link>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
);
|
|
})}
|
|
</div>
|
|
) : (
|
|
<div className="text-center py-20 bg-white rounded-2xl border border-slate-200 shadow-sm space-y-4">
|
|
<SlidersHorizontal className="w-14 h-14 text-slate-300 mx-auto" />
|
|
<h4 className="text-base font-bold text-slate-800">No candidates match your filters</h4>
|
|
<p className="text-xs text-slate-500 max-w-sm mx-auto leading-relaxed">
|
|
Try adjusting your salary limit, adding additional languages, or broadening nationality preferences.
|
|
</p>
|
|
<button
|
|
type="button"
|
|
onClick={resetFilters}
|
|
className="bg-[#185FA5] text-white px-5 py-2.5 rounded-xl text-xs font-bold hover:bg-[#144f8a] transition-all shadow-sm"
|
|
>
|
|
Reset Filters
|
|
</button>
|
|
</div>
|
|
)}
|
|
|
|
{/* Load More Pagination */}
|
|
{filteredWorkers.length > visibleCount && (
|
|
<div className="text-center pt-6">
|
|
<button
|
|
onClick={() => setVisibleCount(visibleCount + 6)}
|
|
className="bg-white border border-slate-200 hover:bg-slate-50 text-[#185FA5] px-8 py-3 rounded-xl text-xs font-bold transition-all shadow-xs"
|
|
>
|
|
Load More Workers
|
|
</button>
|
|
</div>
|
|
)}
|
|
|
|
{/* 5. Worker Comparison Drawer (Col 12 Fixed bottom) */}
|
|
{comparisonIds.length > 0 && (
|
|
<div className="fixed bottom-0 left-0 right-0 bg-white border-t-2 border-[#185FA5] shadow-2xl p-6 z-50 animate-slide-up flex flex-col space-y-4 max-w-7xl mx-auto rounded-t-3xl">
|
|
<div className="flex items-center justify-between">
|
|
<div className="flex items-center space-x-2">
|
|
<span className="w-2.5 h-2.5 rounded-full bg-[#185FA5] animate-ping" />
|
|
<h4 className="font-extrabold text-sm text-slate-900">Comparing Workers ({comparedWorkers.length} of 3)</h4>
|
|
</div>
|
|
<button
|
|
onClick={() => setComparisonIds([])}
|
|
className="text-xs font-bold text-rose-600 hover:underline flex items-center space-x-1"
|
|
>
|
|
<X className="w-4 h-4" />
|
|
<span>Clear all</span>
|
|
</button>
|
|
</div>
|
|
|
|
<div className="grid grid-cols-1 md:grid-cols-3 gap-6">
|
|
{comparedWorkers.map(w => (
|
|
<div key={w.id} className="bg-slate-50 border border-slate-200 rounded-2xl p-4 space-y-3 relative">
|
|
<button
|
|
onClick={() => handleToggleComparison(w.id)}
|
|
className="absolute top-2 right-2 text-slate-400 hover:text-slate-600"
|
|
>
|
|
<X className="w-4 h-4" />
|
|
</button>
|
|
|
|
<div className="flex items-center space-x-2">
|
|
<div className="w-8 h-8 rounded-full bg-blue-100 text-[#185FA5] font-black text-xs flex items-center justify-center overflow-hidden">
|
|
{w.photo ? (
|
|
<img src={w.photo} alt={w.name} className="w-full h-full object-cover" />
|
|
) : (
|
|
<span>{w.name.charAt(0)}</span>
|
|
)}
|
|
</div>
|
|
<div>
|
|
<div className="font-bold text-xs text-slate-900 truncate max-w-[120px]">{w.name}</div>
|
|
<div className="text-[10px] text-slate-500 font-bold">{w.nationality} • {w.category}</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="grid grid-cols-2 gap-2 text-[10px] bg-white p-2.5 rounded-xl border border-slate-100 font-bold text-slate-600">
|
|
<div>Salary: <span className="text-slate-800">{w.salary} AED</span></div>
|
|
<div>Age: <span className="text-slate-800">{w.age} yrs</span></div>
|
|
<div>Job Type: <span className="text-slate-800 uppercase">{w.preferred_job_type}</span></div>
|
|
<div>Status: <span className="text-slate-800 font-black">{w.availability_status}</span></div>
|
|
</div>
|
|
|
|
<div className="space-y-1">
|
|
<div className="text-[8px] font-black text-slate-400 uppercase tracking-wider">Skills</div>
|
|
<div className="flex flex-wrap gap-1">
|
|
{w.skills?.map(skill => (
|
|
<span key={skill} className="px-1.5 py-0.5 bg-blue-50 border border-blue-100 text-[#185FA5] text-[8px] font-black rounded uppercase tracking-wider">
|
|
{skill}
|
|
</span>
|
|
))}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
))}
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
{/* 6. Quick Preview Modal overlay */}
|
|
{previewWorker && (
|
|
<div className="fixed inset-0 bg-slate-900/60 backdrop-blur-xs flex items-center justify-center p-4 z-50 animate-fade-in">
|
|
<div className="bg-white rounded-3xl w-full max-w-lg border border-slate-200 shadow-2xl p-6 relative animate-zoom-in space-y-6">
|
|
<button
|
|
onClick={() => setPreviewWorker(null)}
|
|
className="absolute top-4 right-4 p-2 bg-slate-50 hover:bg-slate-100 rounded-full text-slate-400 hover:text-slate-600 transition-colors"
|
|
>
|
|
<X className="w-5 h-5" />
|
|
</button>
|
|
|
|
<div className="flex items-center space-x-4">
|
|
<div className="w-16 h-16 bg-blue-50 text-[#185FA5] rounded-2xl border border-blue-100 font-black text-2xl flex items-center justify-center shadow-inner overflow-hidden flex-shrink-0">
|
|
{previewWorker.photo ? (
|
|
<img src={previewWorker.photo} alt={previewWorker.name} className="w-full h-full object-cover" />
|
|
) : (
|
|
<User className="w-8 h-8 text-slate-400" />
|
|
)}
|
|
</div>
|
|
<div>
|
|
<div className="flex items-center space-x-1">
|
|
<h4 className="font-extrabold text-lg text-slate-900">{previewWorker.name}</h4>
|
|
{previewWorker.verified && <CheckCircle2 className="w-4 h-4 text-emerald-600" />}
|
|
</div>
|
|
|
|
<div className="flex items-center space-x-1 text-emerald-700 font-black text-[9px] uppercase tracking-wider">
|
|
<ShieldCheck className="w-3.5 h-3.5 text-emerald-600 flex-shrink-0" />
|
|
<span>{previewWorker.emirates_id_status}</span>
|
|
</div>
|
|
<div className="text-xs text-slate-500 font-bold">{previewWorker.nationality} • {previewWorker.category}</div>
|
|
</div>
|
|
</div>
|
|
|
|
<p className="text-xs text-slate-600 italic bg-slate-50 p-4 rounded-xl leading-relaxed">
|
|
"{previewWorker.bio}"
|
|
</p>
|
|
|
|
<div className="grid grid-cols-2 gap-4 text-xs font-bold text-slate-600">
|
|
<div className="p-3 bg-slate-50/50 rounded-xl">
|
|
<div className="text-[10px] text-slate-400 uppercase tracking-widest mb-0.5">Expectations</div>
|
|
<div className="text-slate-800">{previewWorker.salary} AED / mo</div>
|
|
</div>
|
|
<div className="p-3 bg-slate-50/50 rounded-xl">
|
|
<div className="text-[10px] text-slate-400 uppercase tracking-widest mb-0.5">Languages</div>
|
|
<div className="flex flex-wrap gap-1.5 mt-1">
|
|
{previewWorker.languages?.map(lang => (
|
|
<span key={lang} className="text-[9px] bg-white border border-slate-200 text-slate-700 px-2 py-0.5 rounded font-bold flex items-center space-x-1">
|
|
<span>{getLanguageFlag(lang)}</span>
|
|
<span>{lang}</span>
|
|
</span>
|
|
))}
|
|
</div>
|
|
</div>
|
|
<div className="p-3 bg-slate-50/50 rounded-xl">
|
|
<div className="text-[10px] text-slate-400 uppercase tracking-widest mb-0.5">Availability Status</div>
|
|
<span className={`inline-block px-2 py-0.5 text-[8px] font-black rounded-lg uppercase tracking-wider mt-1 border ${
|
|
previewWorker.availability_status === 'Active' ? 'bg-emerald-50 text-emerald-700 border-emerald-200' :
|
|
previewWorker.availability_status === 'Hired' ? 'bg-blue-50 text-blue-700 border-blue-200' :
|
|
'bg-slate-100 text-slate-600 border-slate-200'
|
|
}`}>
|
|
{previewWorker.availability_status}
|
|
</span>
|
|
</div>
|
|
<div className="p-3 bg-slate-50/50 rounded-xl">
|
|
<div className="text-[10px] text-slate-400 uppercase tracking-widest mb-0.5">Job Preference</div>
|
|
<div className="text-slate-800 uppercase text-[10px] mt-1">{previewWorker.preferred_job_type}</div>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Core Platform Skills inside Quick Preview */}
|
|
<div className="space-y-1 pt-1">
|
|
<div className="text-[9px] font-black text-slate-400 uppercase tracking-widest">Platform Skills</div>
|
|
<div className="flex flex-wrap gap-1">
|
|
{previewWorker.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">
|
|
{skill}
|
|
</span>
|
|
))}
|
|
</div>
|
|
</div>
|
|
|
|
<div className="flex items-center space-x-3 pt-2">
|
|
<button
|
|
onClick={() => toggleShortlist(previewWorker.id)}
|
|
className="flex-1 border border-slate-200 hover:bg-slate-50 rounded-xl h-11 text-xs font-bold text-slate-700 flex items-center justify-center space-x-2"
|
|
>
|
|
<Bookmark className="w-4 h-4" />
|
|
<span>{shortlistedIds.includes(previewWorker.id) ? 'Shortlisted ✓' : 'Add to Shortlist'}</span>
|
|
</button>
|
|
<Link
|
|
href={`/employer/workers/${previewWorker.id}`}
|
|
className="flex-1 bg-[#185FA5] hover:bg-[#144f8a] text-white rounded-xl h-11 text-xs font-black flex items-center justify-center shadow-xs"
|
|
>
|
|
View Full Details
|
|
</Link>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
{/* Preset Modal */}
|
|
{showPresetModal && (
|
|
<div className="fixed inset-0 bg-slate-900/60 backdrop-blur-xs flex items-center justify-center p-4 z-50">
|
|
<form onSubmit={saveCurrentPreset} className="bg-white rounded-3xl w-full max-w-sm p-6 relative animate-zoom-in space-y-4 border border-slate-200 shadow-2xl">
|
|
<h4 className="font-extrabold text-sm text-slate-900">Name this search preset</h4>
|
|
<input
|
|
type="text"
|
|
placeholder="e.g. Filipino Nannies, Drivers..."
|
|
value={presetNameInput}
|
|
onChange={(e) => setPresetNameInput(e.target.value)}
|
|
className="w-full p-2.5 border border-slate-300 rounded-xl text-xs focus:outline-none focus:border-[#185FA5]"
|
|
required
|
|
/>
|
|
<div className="flex justify-end space-x-2 pt-2 text-xs font-bold">
|
|
<button
|
|
type="button"
|
|
onClick={() => setShowPresetModal(false)}
|
|
className="px-4 py-2 border border-slate-200 hover:bg-slate-50 rounded-lg"
|
|
>
|
|
Cancel
|
|
</button>
|
|
<button
|
|
type="submit"
|
|
className="px-4 py-2 bg-[#185FA5] text-white hover:bg-[#144f8a] rounded-lg"
|
|
>
|
|
Save Filter Preset
|
|
</button>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
)}
|
|
|
|
</div>
|
|
</EmployerLayout>
|
|
);
|
|
}
|